Compare commits

..

57 commits

Author SHA1 Message Date
7c22dab8a3 nulo: woodpecker CI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-11-25 18:37:24 -03:00
d3a962d068 Dockerfile: rename user to _gitea instead of git 2023-11-25 18:37:11 -03:00
Earl Warren
3380217da1
[TESTS] tests.AddFixtures helper loads additional per-test fixtures
(cherry picked from commit 93a844dd13904c0ba1b7fd4a0a233002194a504b)
(cherry picked from commit 6d6d1a121ce3fc5cf7cd92ad1a38be3bdcbf7088)
(cherry picked from commit 8b101f2860dfbdfd99de71d30740c9e72e1cd9d5)
(cherry picked from commit 3e56212d6d1bca0aecdc1f224c7d78287ef9d35d)
(cherry picked from commit 4f619bc58583892c197ee2588ead929342336217)
2023-11-25 08:08:37 +01:00
Loïc Dachary
e9aa373db5
fix POST /{username}/{reponame}/{type:issues|pulls}/move_pin
(cherry picked from commit 7eda733ed6a22c08a85fdc90deec0c440427cef7)
2023-11-25 08:08:37 +01:00
Loïc Dachary
1e5940b020
test POST /{username}/{reponame}/{type:issues|pulls}/move_pin
(cherry picked from commit 52f50792606a22cbf1e144e1bd480984abf6f53f)
2023-11-25 08:08:37 +01:00
Loïc Dachary
5322136af8
fix GET /api/v1/repos/{owner}/{repo}/keys/{id}
(cherry picked from commit 768238d9f9982e99ad4cbf3942d2d2db5126a150)

Conflicts:
	routers/api/v1/repo/key.go
	trivial context conflict
2023-11-25 08:08:37 +01:00
Loïc Dachary
d095e4fdc5
test GET /api/v1/repos/{owner}/{repo}/keys/{id}
(cherry picked from commit f5ad29dbc77df834a3b5b9a63b19bca680a9f5ed)
2023-11-25 08:08:37 +01:00
Loïc Dachary
a2b1082dda
fix POST /{username}/{reponame}/{tags,release}/delete
(cherry picked from commit a6d2ad6310f754952998fd73118da9f91c563145)
2023-11-25 08:08:37 +01:00
Loïc Dachary
d7b11f5378
test POST /{username}/{reponame}/{tags,release}/delete
(cherry picked from commit 78dcbb62fe87abe044034d880c9e8c22b44c2c98)
2023-11-25 08:08:37 +01:00
Loïc Dachary
5ef4992fd7
fix GET /{username}/{reponame}/{type:issues|pulls}/{index}/content-history/detail
(cherry picked from commit 0853dec293dd632a03948f66af69e75dd582a92d)
2023-11-25 08:08:36 +01:00
Loïc Dachary
75730a6ded
fix POST /{username}/{reponame}/{type:issues|pulls}/{index}/content-history/soft-delete
(cherry picked from commit a11d82a42729eba02032310f7778a9197f4f8ead)
2023-11-25 08:08:36 +01:00
Loïc Dachary
48bcb1937e
fix GET /{owner}/{repo}/comments/{id}/attachments
(cherry picked from commit aed193ef9f5d59aed12cfd7518765d5598c7999f)
2023-11-25 07:23:34 +01:00
Loïc Dachary
4903135a93
test GET /{owner}/{repo}/comments/{id}/attachments
(cherry picked from commit 888dda12cf9bc95f9ef85ba5a518cf40152e07ea)
2023-11-25 07:23:34 +01:00
Loïc Dachary
6f87e71f0c
fix POST /{owner}/{repo}/comments/{id}/reactions/{action}
(cherry picked from commit 21d4556cbeb9d0f825398114ba3a4816f331315b)
2023-11-25 07:23:34 +01:00
Loïc Dachary
5cc6361e31
fix POST /{owner}/{repo}/comments/{id}
(cherry picked from commit 385a1f337462bec34ccc389d4efe21e3b2be8465)
2023-11-25 07:23:34 +01:00
Loïc Dachary
0d7893ca8a
test POST /{owner}/{repo}/comments/{id}
(cherry picked from commit 61db02681a024220d6d2fe61c1479fd03cb341ea)
2023-11-25 07:23:34 +01:00
Loïc Dachary
44f2592028
fix POST /{owner}/{repo}/comments/{id}/delete
(cherry picked from commit 1b57d8493882d9d659164acd3b4a5a99c769d8ed)
2023-11-25 07:23:34 +01:00
Loïc Dachary
d2c16d9c2d
test POST /{owner}/{repo}/comments/{id}/delete
(cherry picked from commit 02da8922f1d9ea8e0985b10a3003315f57b14b46)
2023-11-25 07:23:34 +01:00
Loïc Dachary
0b0b506b74
fix DELETE /api/v1/repos/{owner}/{repo}/issues/comments/{id}
(cherry picked from commit 521eed2312f45bef7de28c9c03c04257862a453c)
2023-11-25 07:23:34 +01:00
Loïc Dachary
939a66e25c
test DELETE /api/v1/repos/{owner}/{repo}/issues/comments/{id}
(cherry picked from commit 11dcaa7ec84bcb2931bfe001d4c6a02c5af4ec5b)
2023-11-25 07:23:33 +01:00
Loïc Dachary
585f74c2ca
fix GET /repos/{owner}/{repo}/issues/comments/{id}/reactions
(cherry picked from commit a146e3d0f9ff8ac1aee4be8a3632c76b35fc3482)
2023-11-25 07:23:33 +01:00
Loïc Dachary
2af5a75d71
test GET /repos/{owner}/{repo}/issues/comments/{id}/reactions
(cherry picked from commit 58d923ccbaad1ec12120800b28dbfe6c8c225556)
2023-11-25 07:23:33 +01:00
Loïc Dachary
685ebdba63
fix {DELETE,POST} /repos/{owner}/{repo}/issues/comments/{id}/reactions
(cherry picked from commit f499075c53752f983c6e4f8af17c449926ba94d9)
2023-11-25 07:23:33 +01:00
Loïc Dachary
f59a6cc0e4
test {DELETE,POST} /repos/{owner}/{repo}/issues/comments/{id}/reactions
(cherry picked from commit ffcd2e79ac3ef63cd33d3ca9a18dae5f16431e54)
2023-11-25 07:23:33 +01:00
Loïc Dachary
e02448bbf5
test GET /api/v1/repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id}
via getIssueCommentSafe

(cherry picked from commit 9a11049715f1194cad777d5dde0ee514fa15d1f1)
2023-11-25 07:23:33 +01:00
Loïc Dachary
e291ea5e33
fix PATCH /api/v1/repos/{owner}/{repo}/issues/comments/{id}
(cherry picked from commit 51c280e877765efe721e607aa95bcbb5aef364e0)
2023-11-25 07:23:33 +01:00
Loïc Dachary
8726ce2635
test PATCH /api/v1/repos/{owner}/{repo}/issues/comments/{id}
(cherry picked from commit 362f340ed9ee28627140ca06dd7487a8989ef62b)
2023-11-25 07:23:33 +01:00
Loïc Dachary
3ddfca10ac
fix API usage of a PR index in place of issue index and vice versa
(cherry picked from commit 7b95266de083c8de0ff224530a9b69e82c52c344)
2023-11-25 07:23:32 +01:00
Loïc Dachary
6b4cb070cc
enforce reqRepoReader(unit.TypeIssues) POST /repos/{owner}/{repo}/issues
(cherry picked from commit d3db2fa8bc85e9d67f30854bba0a4c1e8b57b015)
2023-11-25 07:23:32 +01:00
Loïc Dachary
c70eb32280
enforce reqRepoReader(unit.TypeIssues) GET /repos/{owner}/{repo}/issues/pinned
(cherry picked from commit 00fad97fc1b27db40a002c9ab3f709d04dc2cdd1)
2023-11-25 07:23:32 +01:00
Giteabot
c0ccd4c2d7
Fix no ActionTaskOutput table waring (#28149) (#28151)
Backport #28149 by @yp05327

Reproduce:
- Create a new Gitea instance
- Register a runner
- Create a repo and add a workflow
- Check the log, you will see warnings:

![image](https://github.com/go-gitea/gitea/assets/18380374/5f1278e0-114b-48bc-8113-8ba1404d9975)
It comes from:

![image](https://github.com/go-gitea/gitea/assets/18380374/c2807831-e137-4229-9536-87f6114c8a5b)

The reason is that we forgot registering `ActionTaskOutput` model.
So `action_table_output` table will be missing in your db.

Co-authored-by: yp05327 <576951401@qq.com>
(cherry picked from commit 41b2d0be931dcac7d372efb0f8207fcb8379fce1)
2023-11-22 17:23:43 +01:00
Giteabot
f302373eb4
Restricted users only see repos in orgs which their team was assigned to (#28025) (#28050)
Backport #28025 by @6543

---
*Sponsored by Kithara Software GmbH*

Co-authored-by: 6543 <m.huber@kithara.com>
(cherry picked from commit 439e071acf8d7a38b78888915422490a2a462f8a)
2023-11-22 17:23:33 +01:00
Loïc Dachary
5d18f4b19f
[BRANDING] X-Forgejo-OTP can be used instead of X-Gitea-OTP
(cherry picked from commit 7b0549cd70aa7cafec853e15b25270847c59850b)
(cherry picked from commit 13e10a65d974c7b594681bfa36402a6144862116)
(cherry picked from commit 65bdd73cf27895a9fb8db2a95ef4f5b08951481d)
(cherry picked from commit 64eba8bb923176b4c286b1d0c83792f3c3005ca8)
(cherry picked from commit 4c49b1a759abe3604afc1121e83c9a942016ad6a)
(cherry picked from commit 93b4d0640683ea986657453b1fce49a00c861764)
(cherry picked from commit e2bc5f36d958f4349160ec145719c302d4023cd0)
(cherry picked from commit 2bee76f9dfa998c83ea4fe648997fad0b6224fa9)
(cherry picked from commit 3d8a1b4a9fb9dc55bbd62fd8855ea85e58dc263f)
(cherry picked from commit 99dd092cd02d7af8374acf454833ce1c05fd4fd9)
(cherry picked from commit 0fdbd02204d533f907cd22c83c73bf0156ec4a88)
(cherry picked from commit 70b277a183c0d85966fa84e9b054f164ae2d2a44)
(cherry picked from commit 3eece7fbb4e67d970d8979d0d60a58ee2a195ea5)
(cherry picked from commit 4838fc9e1145a74c56926de68854234604b5e38f)
(cherry picked from commit b76ed541cf4d73702a83d6b96f8618b6f8c44393)
(cherry picked from commit dcdfb5b65c6fbf50798a0c49d0f879dd1285ee41)
(cherry picked from commit 377dc48cdc3b1c2bcc95f86a7bf3602468ac5c39)
(cherry picked from commit acc862f411c79f7832c8ba2c182af738f25f4f8b)
(cherry picked from commit ac75ef101f89d58442760cec21a3f3f9199d4710)
(cherry picked from commit 08f2d9f7c5b0d51358b009b0b38b626b231ec32b)
(cherry picked from commit e4096f0b6441ba68719146e5a48ef44233e27a86)
(cherry picked from commit bf5876f06224ac90e931f2f47b66a5b9c38b2a87)
(cherry picked from commit 7dc60637e5e097b5dbc38e068ee7ba553385b496)
(cherry picked from commit ef3101774ba5083e259d84db9997ff0aaddab14c)
(cherry picked from commit ecb9e8867c3503387cbaf97df27d8c60a840f4a4)
(cherry picked from commit 64f0ae72fec30ea443d73f8566c140682e7b9838)
(cherry picked from commit 8dd6ec786294741361f79c08b0c051d2258bda02)
(cherry picked from commit b36723e52b975d2e57af363db1d9118f48feade1)

Conflicts:
	modules/context/api.go
	https://codeberg.org/forgejo/forgejo/pulls/1466
(cherry picked from commit 5c378e0cb823f2bad52224859ca326afb33bfd4b)
(cherry picked from commit 1d87602819be9f87bf9d06203c37160568c18e78)
(cherry picked from commit 0f72002d667224a75a4924ebb5557eca8bddbe70)
(cherry picked from commit da2556eb13a2c976d1630315dbee8c3bc5444a11)
(cherry picked from commit c01688cd900369b8cbed961f6a841ea536b07207)
(cherry picked from commit af4bba832962ce4db3327c140283ce5b8d2cf6a5)
(cherry picked from commit 33ca322c2ea7b05fcab084e06f8b3a6d65125808)

Conflicts:
	modules/context/api.go
	https://codeberg.org/forgejo/forgejo/pulls/1739
(cherry picked from commit c18e374d4481592681ae127b723f11076c37bb91)
(cherry picked from commit 27c4797c9fb3c42be252223ac0add0605f18acba)
2023-11-14 13:17:12 +01:00
Giteabot
d7408d8b0b
Dont leak private users via extensions (#28023) (#28028)
Backport #28023 by @6543

there was no check in place if a user could see a other user, if you
append e.g. `.rss`

(cherry picked from commit 69ea554e2362e5c4943c2463c2ec547bf631f18b)
2023-11-14 13:17:12 +01:00
Nanguan Lin
6dfe993913
Fix wrong xorm Delete usage(backport for 1.20) (#28003)
manually backport for https://github.com/go-gitea/gitea/pull/27995
The conflict is `ctx` and `db.Defaultctx`.

(cherry picked from commit c077a084d7bac8acc1bd247b2bd3d60835a17ded)
2023-11-14 13:17:12 +01:00
Giteabot
1bbc1adcdc
Render email addresses as such if followed by punctuation (#27987) (#27991)
Backport #27987 by @yardenshoham

Added the following characters to the regular expression for the email:

- ,
- ;
- ?
- !

Also added a test case.

- Fixes #27616

# Before

![image](https://github.com/go-gitea/gitea/assets/20454870/c57eac26-f281-43ef-a51d-9c9a81b63efa)

# After

![image](https://github.com/go-gitea/gitea/assets/20454870/fc7d5c08-4350-4af0-a7f0-d1444d2d75af)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Yarden Shoham <git@yardenshoham.com>
(cherry picked from commit dfd960f22a7dafaa65b9f96e61ec8bef9ea5ea69)
2023-11-14 13:17:12 +01:00
Nanguan Lin
d610ea3fbb
Remove duplicated button in Install web page (#27941)
Fix #27934
Regression #25648

(cherry picked from commit 2978b435bb5b272e4c2ed7252f26a3348f2453fb)
2023-11-14 13:17:12 +01:00
KN4CK3R
44df78edd4
Unify two factor check (#27915) (#27939)
Backport of #27915

Fixes #27819

We have support for two factor logins with the normal web login and with
basic auth. For basic auth the two factor check was implemented at three
different places and you need to know that this check is necessary. This
PR moves the check into the basic auth itself.

(cherry picked from commit 00705da102be929dfa41519b030be3bdd8c68472)
2023-11-14 13:17:12 +01:00
Giteabot
1fd3cc3217
Fix DownloadFunc when migrating releases (#27887) (#27889)
Backport #27887 by @Zettat123

We should not use `asset.ID` in DownloadFunc because DownloadFunc is a
closure.

1bf5527eac/services/migrations/gitea_downloader.go (L284-L295)

A similar bug when migrating from GitHub has been fixed in #14703. This
PR fixes the bug when migrating from Gitea and GitLab.

Co-authored-by: Zettat123 <zettat123@gmail.com>
(cherry picked from commit 4a48370d91354c2857ade10a177c8827b5866e4c)
2023-11-14 13:17:12 +01:00
Lunny Xiao
f2c3491b61
Fix http protocol auth (#27875) (#27878)
backport #27875

(cherry picked from commit 1dedf9bba0bf909f9e275565604ec8f2adb5a86e)
2023-11-14 13:17:12 +01:00
Giteabot
713652e3d8
Fix package webhook (#27839) (#27854)
Backport #27839 by @lunny

Fix #23742

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
(cherry picked from commit 2147bfde0573a2f2492ca0c78c2e042cf327903a)
2023-11-14 13:17:12 +01:00
Lunny Xiao
b4fb797b32
Revert "fix orphan check for deleted branch (#27310) (#27320)" (#27763)
Because branch table is created until 1.21
Fix #27508

(cherry picked from commit a1c232cae3d8827691297d02d6f4ba980a805cd2)
2023-11-14 13:17:12 +01:00
Giteabot
2a5d5da930
Fix label render containing invalid HTML (#27752) (#27761)
Backport #27752 by @earl-warren

- The label HTML contained a quote that wasn't being closed.

Refs: https://codeberg.org/forgejo/forgejo/pulls/1651

(cherry picked from commit e2bc2c9a1fff482c49dbeb3a51e4e1c698bf506c)

Co-authored-by: Earl Warren <109468362+earl-warren@users.noreply.github.com>
Co-authored-by: Gusted <postmaster@gusted.xyz>
(cherry picked from commit 63512cd15d14254beadc0fe105d4239708fb758d)
2023-11-14 13:17:12 +01:00
Giteabot
64373004b5
Fix org team endpoint (#27721) (#27729)
Backport #27721 by @lng2020

Fix #27711

Co-authored-by: Nanguan Lin <70063547+lng2020@users.noreply.github.com>
(cherry picked from commit 71803d33e395829e4b7cee2bd4ae078527106a48)
2023-11-14 13:17:11 +01:00
Giteabot
2a321fcfda
Adapt .changelog.yml to new labeling system (#27701) (#27708)
Backport #27701 by @delvh

Otherwise, it is not possible anymore to generate changelogs.

Co-authored-by: delvh <dev.lh@web.de>
(cherry picked from commit a954cc3fb9d396af61e5d7af7a88d6ebe3abb80b)
2023-11-14 13:17:11 +01:00
Giteabot
d6798ae015
Support allowed hosts for webhook to work with proxy (#27655) (#27674)
Backport #27655 by @wolfogre

When `webhook.PROXY_URL` has been set, the old code will check if the
proxy host is in `ALLOWED_HOST_LIST` or reject requests through the
proxy. It requires users to add the proxy host to `ALLOWED_HOST_LIST`.
However, it actually allows all requests to any port on the host, when
the proxy host is probably an internal address.

But things may be even worse. `ALLOWED_HOST_LIST` doesn't really work
when requests are sent to the allowed proxy, and the proxy could forward
them to any hosts.

This PR fixes it by:

- If the proxy has been set, always allow connectioins to the host and
port.
- Check `ALLOWED_HOST_LIST` before forwarding.

Co-authored-by: Jason Song <i@wolfogre.com>
(cherry picked from commit ca4418eff12d92a4da29bba4331451bf6cd0b620)
2023-11-14 13:17:11 +01:00
Giteabot
cf1174acbf
Fix poster is not loaded in get default merge message (#27657) (#27665)
Backport #27657 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
(cherry picked from commit 80c0c8815203128703eae741e712289393458687)
2023-11-14 13:17:11 +01:00
Giteabot
62c33f92a9
Fix 404 when deleting Docker package with an internal version (#27615) (#27629)
Backport #27615 by @lng2020

close #27601
The Docker registry has an internal version, which leads to 404

Co-authored-by: Nanguan Lin <70063547+lng2020@users.noreply.github.com>
(cherry picked from commit 171950a0d45745743d519aeb547b2a93cfb6410d)
2023-11-14 13:17:11 +01:00
Giteabot
f142ae18c0
Fix attachment download bug (#27486) (#27570)
Backport #27486 by @lunny

Fix #27204

This PR allows `/<username>/<reponame>/attachments/<uuid>` access with
personal access token and also changed attachments API download url to
it so it can be download correctly.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
(cherry picked from commit 7b96f71bc713b937363ab71abd383fbb79d89216)
2023-11-14 13:17:11 +01:00
yp05327
2e50870688
Avoid run change title process when the title is same (#27467) (#27557)
Backport #27467 manually.

(cherry picked from commit e6d1afaee33bac32c905c15c15909ee22f63c9a6)
2023-11-14 13:17:11 +01:00
silverwind
2716e2f626
Fix mermaid flowchart margin issue (#27503) (#27517)
Backport https://github.com/go-gitea/gitea/pull/27503 to 1.20

Fixes: https://github.com/go-gitea/gitea/issues/27435
Related: https://github.com/mermaid-js/mermaid/issues/4907

<img width="924" alt="image"

src="https://github.com/go-gitea/gitea/assets/115237/494a1d2e-4c56-48d0-9843-82a5e5aa977e">

(cherry picked from commit 1d4c193df588c0141fad456f1c17b8dfaf733265)
2023-11-14 13:17:11 +01:00
Giteabot
e0fe8a8ab4
Fix panic in storageHandler (#27446) (#27478)
Backport #27446 by @sryze

storageHandler() is written as a middleware but is used as an endpoint
handler, and thus `next` is actually `nil`, which causes a null pointer
dereference when a request URL does not match the pattern (where it
calls `next.ServerHTTP()`).

Example CURL command to trigger the panic:

```
curl -I "http://yourhost/gitea//avatars/a"
```

Fixes #27409

---

Note: the diff looks big but it's actually a small change - all I did
was to remove the outer closure (and one level of indentation) ~and
removed the HTTP method and pattern checks as they seem redundant
because go-chi already does those checks~. You might want to check "Hide
whitespace" when reviewing it.

Alternative solution (a bit simpler): append `, misc.DummyOK` to the
route declarations that utilize `storageHandler()` - this makes it
return an empty response when the URL is invalid. I've tested this one
and it works too. Or maybe it would be better to return a 400 error in
that case (?)

Co-authored-by: Sergey Zolotarev <sryze@outlook.com>
(cherry picked from commit 4ffa683820188175570ea3a0faf9d93046042b91)
2023-11-14 13:17:11 +01:00
Giteabot
c50af699ea
When comparing with an non-exist repository, return 404 but 500 (#27437) (#27441)
Backport #27437 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
(cherry picked from commit 973b7f62989d16960fa918f5758ff2998317c352)
2023-11-14 13:17:11 +01:00
Lunny Xiao
915c60f8c1
Add 1.20.5 changelog (#27404)
(cherry picked from commit 4126aad4aa12f3dbd4a21063d266d096ee6bb52f)
2023-11-14 13:17:11 +01:00
Earl Warren
a1e6944bd7
Revert "[BRANDING] X-Forgejo-OTP can be used instead of X-Gitea-OTP"
This reverts commit 9413fd0274.
2023-11-14 13:17:11 +01:00
Earl Warren
d7e67cf616
[SEMVER] 5.0.6+0-gitea-1.20.5 2023-11-14 13:17:11 +01:00
Earl Warren
ee48c0d5ea
[CI] Forgejo Actions based CI for PR & branches (squash) use node:20-bookworm
No longer use the custom test-env image, it is unecessary technical
debt.

Also upgrade to bitnami/minio:2023.8.31 to align with what Gitea tests

(cherry picked from commit d9b77fd2735a52043b4f8f1baaaa2e15073db621)

Conflicts:
	.forgejo/workflows/testing.yml
	* mysql was mysql-8 in v1.21 and below
	* No MINIO testing
	* go 1.20 instead of go 1.21
2023-10-20 17:30:34 +02:00
73 changed files with 975 additions and 281 deletions

View file

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

View file

@ -10,6 +10,8 @@ on:
jobs: jobs:
lint-backend: lint-backend:
runs-on: docker runs-on: docker
container:
image: 'docker.io/node:20-bookworm'
steps: steps:
- uses: https://code.forgejo.org/actions/checkout@v3 - uses: https://code.forgejo.org/actions/checkout@v3
- uses: https://code.forgejo.org/actions/setup-go@v4 - uses: https://code.forgejo.org/actions/setup-go@v4
@ -22,6 +24,8 @@ jobs:
TAGS: bindata sqlite sqlite_unlock_notify TAGS: bindata sqlite sqlite_unlock_notify
checks-backend: checks-backend:
runs-on: docker runs-on: docker
container:
image: 'docker.io/node:20-bookworm'
steps: steps:
- uses: https://code.forgejo.org/actions/checkout@v3 - uses: https://code.forgejo.org/actions/checkout@v3
- uses: https://code.forgejo.org/actions/setup-go@v4 - uses: https://code.forgejo.org/actions/setup-go@v4
@ -34,7 +38,7 @@ jobs:
runs-on: docker runs-on: docker
needs: [lint-backend, checks-backend] needs: [lint-backend, checks-backend]
container: container:
image: codeberg.org/forgejo/test_env:1.20 image: 'docker.io/node:20-bookworm'
steps: steps:
- uses: https://code.forgejo.org/actions/checkout@v3 - uses: https://code.forgejo.org/actions/checkout@v3
- uses: https://code.forgejo.org/actions/setup-go@v4 - uses: https://code.forgejo.org/actions/setup-go@v4
@ -42,15 +46,16 @@ jobs:
go-version: "1.20" go-version: "1.20"
- run: | - run: |
git config --add safe.directory '*' git config --add safe.directory '*'
chown -R gitea:gitea . /go adduser --quiet --comment forgejo --disabled-password forgejo
chown -R forgejo:forgejo .
- run: | - run: |
su gitea -c 'make deps-backend' su forgejo -c 'make deps-backend'
- run: | - run: |
su gitea -c 'make backend' su forgejo -c 'make backend'
env: env:
TAGS: bindata TAGS: bindata
- run: | - run: |
su gitea -c 'make unit-test-coverage test-check' su forgejo -c 'make unit-test-coverage test-check'
timeout-minutes: 50 timeout-minutes: 50
env: env:
RACE_ENABLED: 'true' RACE_ENABLED: 'true'
@ -59,7 +64,7 @@ jobs:
runs-on: docker runs-on: docker
needs: [lint-backend, checks-backend] needs: [lint-backend, checks-backend]
container: container:
image: codeberg.org/forgejo/test_env:1.20 image: 'docker.io/node:20-bookworm'
services: services:
mysql8: mysql8:
image: mysql:8-debian image: mysql:8-debian
@ -77,17 +82,24 @@ jobs:
- uses: https://code.forgejo.org/actions/setup-go@v4 - uses: https://code.forgejo.org/actions/setup-go@v4
with: with:
go-version: "1.20" go-version: "1.20"
- run: | - name: install dependencies
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install --no-install-recommends -qq -y git-lfs
- name: setup user and permissions
run: |
git config --add safe.directory '*' git config --add safe.directory '*'
chown -R gitea:gitea . /go adduser --quiet --comment forgejo --disabled-password forgejo
chown -R forgejo:forgejo .
- run: | - run: |
su gitea -c 'make deps-backend' su forgejo -c 'make deps-backend'
- run: | - run: |
su gitea -c 'make backend' su forgejo -c 'make backend'
env: env:
TAGS: bindata TAGS: bindata
- run: | - run: |
su gitea -c 'make test-mysql8-migration test-mysql8' su forgejo -c 'make test-mysql8-migration test-mysql8'
timeout-minutes: 50 timeout-minutes: 50
env: env:
TAGS: bindata TAGS: bindata
@ -96,10 +108,10 @@ jobs:
runs-on: docker runs-on: docker
needs: [lint-backend, checks-backend] needs: [lint-backend, checks-backend]
container: container:
image: codeberg.org/forgejo/test_env:1.20 image: 'docker.io/node:20-bookworm'
services: services:
pgsql: pgsql:
image: postgres:15 image: 'docker.io/postgres:15'
env: env:
POSTGRES_DB: test POSTGRES_DB: test
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
@ -110,17 +122,24 @@ jobs:
- uses: https://code.forgejo.org/actions/setup-go@v4 - uses: https://code.forgejo.org/actions/setup-go@v4
with: with:
go-version: "1.20" go-version: "1.20"
- run: | - name: install dependencies
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install --no-install-recommends -qq -y git-lfs
- name: setup user and permissions
run: |
git config --add safe.directory '*' git config --add safe.directory '*'
chown -R gitea:gitea . /go adduser --quiet --comment forgejo --disabled-password forgejo
chown -R forgejo:forgejo .
- run: | - run: |
su gitea -c 'make deps-backend' su forgejo -c 'make deps-backend'
- run: | - run: |
su gitea -c 'make backend' su forgejo -c 'make backend'
env: env:
TAGS: bindata TAGS: bindata
- run: | - run: |
su gitea -c 'make test-pgsql-migration test-pgsql' su forgejo -c 'make test-pgsql-migration test-pgsql'
timeout-minutes: 50 timeout-minutes: 50
env: env:
TAGS: bindata gogit TAGS: bindata gogit
@ -131,23 +150,30 @@ jobs:
runs-on: docker runs-on: docker
needs: [lint-backend, checks-backend] needs: [lint-backend, checks-backend]
container: container:
image: codeberg.org/forgejo/test_env:1.20 image: 'docker.io/node:20-bookworm'
steps: steps:
- uses: https://code.forgejo.org/actions/checkout@v3 - uses: https://code.forgejo.org/actions/checkout@v3
- uses: https://code.forgejo.org/actions/setup-go@v4 - uses: https://code.forgejo.org/actions/setup-go@v4
with: with:
go-version: "1.20" go-version: "1.20"
- run: | - name: install dependencies
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install --no-install-recommends -qq -y git-lfs
- name: setup user and permissions
run: |
git config --add safe.directory '*' git config --add safe.directory '*'
chown -R gitea:gitea . /go adduser --quiet --comment forgejo --disabled-password forgejo
chown -R forgejo:forgejo .
- run: | - run: |
su gitea -c 'make deps-backend' su forgejo -c 'make deps-backend'
- run: | - run: |
su gitea -c 'make backend' su forgejo -c 'make backend'
env: env:
TAGS: bindata gogit sqlite sqlite_unlock_notify TAGS: bindata gogit sqlite sqlite_unlock_notify
- run: | - run: |
su gitea -c 'make test-sqlite-migration test-sqlite' su forgejo -c 'make test-sqlite-migration test-sqlite'
timeout-minutes: 50 timeout-minutes: 50
env: env:
TAGS: bindata gogit sqlite sqlite_unlock_notify TAGS: bindata gogit sqlite sqlite_unlock_notify

View file

@ -3,7 +3,7 @@ pipeline:
image: docker.io/woodpeckerci/plugin-docker-buildx image: docker.io/woodpeckerci/plugin-docker-buildx
settings: settings:
repo: gitea.nulo.in/nulo/forgejo repo: gitea.nulo.in/nulo/forgejo
tag: v1.20.5-0 tag: v1.20.5-1
registry: https://gitea.nulo.in registry: https://gitea.nulo.in
username: Nulo username: Nulo
password: password:

View file

@ -4,6 +4,33 @@ This changelog goes through all the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.com). been added to each release, please refer to the [blog](https://blog.gitea.com).
## [1.20.5](https://github.com/go-gitea/gitea/releases/tag/1.20.5) - 2023-10-03
* ENHANCEMENTS
* Fix z-index on markdown completion (#27237) (#27242 & #27238)
* Use secure cookie for HTTPS sites (#26999) (#27013)
* BUGFIXES
* Fix git 2.11 error when checking IsEmpty (#27393) (#27396)
* Allow get release download files and lfs files with oauth2 token format (#26430) (#27378)
* Fix orphan check for deleted branch (#27310) (#27320)
* Quote table `release` in sql queries (#27205) (#27219)
* Fix release URL in webhooks (#27182) (#27184)
* Fix successful return value for `SyncAndGetUserSpecificDiff` (#27152) (#27156)
* fix pagination for followers and following (#27127) (#27138)
* Fix issue templates when blank isses are disabled (#27061) (#27082)
* Fix context cache bug & enable context cache for dashabord commits' authors(#26991) (#27017)
* Fix INI parsing for value with trailing slash (#26995) (#27001)
* Fix PushEvent NullPointerException jenkinsci/github-plugin (#27203) (#27249)
* Fix organization field being null in POST /orgs/{orgid}/teams (#27150) (#27167 & #27162)
* Fix bug of review request number (#27406) (#27104)
* TESTING
* services/wiki: Close() after error handling (#27129) (#27137)
* DOCS
* Improve actions docs related to `pull_request` event (#27126) (#27145)
* MISC
* Add logs for data broken of comment review (#27326) (#27344)
* Load reviewer before sending notification (#27063) (#27064)
## [1.20.4](https://github.com/go-gitea/gitea/releases/tag/v1.20.4) - 2023-09-08 ## [1.20.4](https://github.com/go-gitea/gitea/releases/tag/v1.20.4) - 2023-09-08
* SECURITY * SECURITY

View file

@ -89,7 +89,7 @@ endif
VERSION = ${GITEA_VERSION} VERSION = ${GITEA_VERSION}
# SemVer # SemVer
FORGEJO_VERSION := 5.0.5+0-gitea-1.20.5 FORGEJO_VERSION := 5.0.6+0-gitea-1.20.5
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" -X "code.gitea.io/gitea/routers/api/forgejo/v1.ForgejoVersion=$(FORGEJO_VERSION)" -X "main.ForgejoVersion=$(FORGEJO_VERSION)" LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" -X "code.gitea.io/gitea/routers/api/forgejo/v1.ForgejoVersion=$(FORGEJO_VERSION)" -X "main.ForgejoVersion=$(FORGEJO_VERSION)"

View file

@ -20,6 +20,10 @@ type ActionTaskOutput struct {
OutputValue string `xorm:"MEDIUMTEXT"` OutputValue string `xorm:"MEDIUMTEXT"`
} }
func init() {
db.RegisterModel(new(ActionTaskOutput))
}
// FindTaskOutputByTaskID returns the outputs of the task. // FindTaskOutputByTaskID returns the outputs of the task.
func FindTaskOutputByTaskID(ctx context.Context, taskID int64) ([]*ActionTaskOutput, error) { func FindTaskOutputByTaskID(ctx context.Context, taskID int64) ([]*ActionTaskOutput, error) {
var outputs []*ActionTaskOutput var outputs []*ActionTaskOutput

View file

@ -232,7 +232,7 @@ func CreateSource(source *Source) error {
err = registerableSource.RegisterSource() err = registerableSource.RegisterSource()
if err != nil { if err != nil {
// remove the AuthSource in case of errors while registering configuration // remove the AuthSource in case of errors while registering configuration
if _, err := db.GetEngine(db.DefaultContext).Delete(source); err != nil { if _, err := db.GetEngine(db.DefaultContext).ID(source.ID).Delete(new(Source)); err != nil {
log.Error("CreateSource: Error while wrapOpenIDConnectInitializeError: %v", err) log.Error("CreateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
} }
} }

View file

@ -1014,6 +1014,7 @@ type FindCommentsOptions struct {
Type CommentType Type CommentType
IssueIDs []int64 IssueIDs []int64
Invalidated util.OptionalBool Invalidated util.OptionalBool
IsPull util.OptionalBool
} }
// ToConds implements FindOptions interface // ToConds implements FindOptions interface
@ -1048,6 +1049,9 @@ func (opts *FindCommentsOptions) ToConds() builder.Cond {
if !opts.Invalidated.IsNone() { if !opts.Invalidated.IsNone() {
cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()}) cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()})
} }
if opts.IsPull != util.OptionalBoolNone {
cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()})
}
return cond return cond
} }
@ -1055,7 +1059,7 @@ func (opts *FindCommentsOptions) ToConds() builder.Cond {
func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) { func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) {
comments := make([]*Comment, 0, 10) comments := make([]*Comment, 0, 10)
sess := db.GetEngine(ctx).Where(opts.ToConds()) sess := db.GetEngine(ctx).Where(opts.ToConds())
if opts.RepoID > 0 { if opts.RepoID > 0 || opts.IsPull != util.OptionalBoolNone {
sess.Join("INNER", "issue", "issue.id = comment.issue_id") sess.Join("INNER", "issue", "issue.id = comment.issue_id")
} }

View file

@ -637,12 +637,12 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu
userOrgTeamUnitRepoCond("`repository`.id", user.ID, unitType), userOrgTeamUnitRepoCond("`repository`.id", user.ID, unitType),
) )
} }
cond = cond.Or(
// 4. Repositories that we directly own // 4. Repositories that we directly own
builder.Eq{"`repository`.owner_id": user.ID}, cond = cond.Or(builder.Eq{"`repository`.owner_id": user.ID})
if !user.IsRestricted {
// 5. Be able to see all public repos in private organizations that we are an org_user of // 5. Be able to see all public repos in private organizations that we are an org_user of
userOrgPublicRepoCond(user.ID), cond = cond.Or(userOrgPublicRepoCond(user.ID))
) }
} }
return cond return cond

View file

@ -7,6 +7,7 @@ package unittest
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"time" "time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -28,6 +29,16 @@ func GetXORMEngine(engine ...*xorm.Engine) (x *xorm.Engine) {
return db.DefaultContext.(*db.Context).Engine().(*xorm.Engine) return db.DefaultContext.(*db.Context).Engine().(*xorm.Engine)
} }
func OverrideFixtures(opts FixturesOptions, engine ...*xorm.Engine) func() {
old := fixturesLoader
if err := InitFixtures(opts, engine...); err != nil {
panic(err)
}
return func() {
fixturesLoader = old
}
}
// InitFixtures initialize test fixtures for a test database // InitFixtures initialize test fixtures for a test database
func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
e := GetXORMEngine(engine...) e := GetXORMEngine(engine...)
@ -37,6 +48,12 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
} else { } else {
fixtureOptionFiles = testfixtures.Files(opts.Files...) fixtureOptionFiles = testfixtures.Files(opts.Files...)
} }
var fixtureOptionDirs []func(*testfixtures.Loader) error
if opts.Dirs != nil {
for _, dir := range opts.Dirs {
fixtureOptionDirs = append(fixtureOptionDirs, testfixtures.Directory(filepath.Join(opts.Base, dir)))
}
}
dialect := "unknown" dialect := "unknown"
switch e.Dialect().URI().DBType { switch e.Dialect().URI().DBType {
case schemas.POSTGRES: case schemas.POSTGRES:
@ -57,6 +74,7 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
testfixtures.DangerousSkipTestDatabaseCheck(), testfixtures.DangerousSkipTestDatabaseCheck(),
fixtureOptionFiles, fixtureOptionFiles,
} }
loaderOptions = append(loaderOptions, fixtureOptionDirs...)
if e.Dialect().URI().DBType == schemas.POSTGRES { if e.Dialect().URI().DBType == schemas.POSTGRES {
loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences()) loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences())

View file

@ -198,6 +198,8 @@ func MainTest(m *testing.M, testOpts *TestOptions) {
type FixturesOptions struct { type FixturesOptions struct {
Dir string Dir string
Files []string Files []string
Dirs []string
Base string
} }
// CreateTestEngine creates a memory database and loads the fixture data from fixturesDir // CreateTestEngine creates a memory database and loads the fixture data from fixturesDir

View file

@ -11,7 +11,6 @@ import (
"net/url" "net/url"
"strings" "strings"
"code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -197,39 +196,6 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
} }
} }
func getOtpHeader(header http.Header) string {
otpHeader := header.Get("X-Gitea-OTP")
if forgejoHeader := header.Get("X-Forgejo-OTP"); forgejoHeader != "" {
otpHeader = forgejoHeader
}
return otpHeader
}
// CheckForOTP validates OTP
func (ctx *APIContext) CheckForOTP() {
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
return // Skip 2FA
}
twofa, err := auth.GetTwoFactorByUID(ctx.Doer.ID)
if err != nil {
if auth.IsErrTwoFactorNotEnrolled(err) {
return // No 2FA enrollment for this user
}
ctx.Error(http.StatusInternalServerError, "GetTwoFactorByUID", err)
return
}
ok, err := twofa.ValidateTOTP(getOtpHeader(ctx.Req.Header))
if err != nil {
ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err)
return
}
if !ok {
ctx.Error(http.StatusUnauthorized, "", nil)
return
}
}
// APIContexter returns apicontext as middleware // APIContexter returns apicontext as middleware
func APIContexter() func(http.Handler) http.Handler { func APIContexter() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {

View file

@ -1,23 +0,0 @@
// SPDX-License-Identifier: MIT
package context
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetOtpHeader(t *testing.T) {
header := http.Header{}
assert.EqualValues(t, "", getOtpHeader(header))
// Gitea
giteaOtp := "123456"
header.Set("X-Gitea-OTP", giteaOtp)
assert.EqualValues(t, giteaOtp, getOtpHeader(header))
// Forgejo has precedence
forgejoOtp := "abcdef"
header.Set("X-Forgejo-OTP", forgejoOtp)
assert.EqualValues(t, forgejoOtp, getOtpHeader(header))
}

View file

@ -168,9 +168,9 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
// find protected branches without existing repository // find protected branches without existing repository
genericOrphanCheck("Protected Branches without existing repository", genericOrphanCheck("Protected Branches without existing repository",
"protected_branch", "repository", "protected_branch.repo_id=repository.id"), "protected_branch", "repository", "protected_branch.repo_id=repository.id"),
// find branches without existing repository // find deleted branches without existing repository
genericOrphanCheck("Branches without existing repository", genericOrphanCheck("Deleted Branches without existing repository",
"branch", "repository", "branch.repo_id=repository.id"), "deleted_branch", "repository", "deleted_branch.repo_id=repository.id"),
// find LFS locks without existing repository // find LFS locks without existing repository
genericOrphanCheck("LFS locks without existing repository", genericOrphanCheck("LFS locks without existing repository",
"lfs_lock", "repository", "lfs_lock.repo_id=repository.id"), "lfs_lock", "repository", "lfs_lock.repo_id=repository.id"),

View file

@ -7,12 +7,17 @@ import (
"context" "context"
"fmt" "fmt"
"net" "net"
"net/url"
"syscall" "syscall"
"time" "time"
) )
// NewDialContext returns a DialContext for Transport, the DialContext will do allow/block list check // NewDialContext returns a DialContext for Transport, the DialContext will do allow/block list check
func NewDialContext(usage string, allowList, blockList *HostMatchList) func(ctx context.Context, network, addr string) (net.Conn, error) { func NewDialContext(usage string, allowList, blockList *HostMatchList) func(ctx context.Context, network, addr string) (net.Conn, error) {
return NewDialContextWithProxy(usage, allowList, blockList, nil)
}
func NewDialContextWithProxy(usage string, allowList, blockList *HostMatchList, proxy *url.URL) func(ctx context.Context, network, addr string) (net.Conn, error) {
// How Go HTTP Client works with redirection: // How Go HTTP Client works with redirection:
// transport.RoundTrip URL=http://domain.com, Host=domain.com // transport.RoundTrip URL=http://domain.com, Host=domain.com
// transport.DialContext addrOrHost=domain.com:80 // transport.DialContext addrOrHost=domain.com:80
@ -26,11 +31,18 @@ func NewDialContext(usage string, allowList, blockList *HostMatchList) func(ctx
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second, KeepAlive: 30 * time.Second,
Control: func(network, ipAddr string, c syscall.RawConn) (err error) { Control: func(network, ipAddr string, c syscall.RawConn) error {
var host string host, port, err := net.SplitHostPort(addrOrHost)
if host, _, err = net.SplitHostPort(addrOrHost); err != nil { if err != nil {
return err return err
} }
if proxy != nil {
// Always allow the host of the proxy, but only on the specified port.
if host == proxy.Hostname() && port == proxy.Port() {
return nil
}
}
// in Control func, the addr was already resolved to IP:PORT format, there is no cost to do ResolveTCPAddr here // in Control func, the addr was already resolved to IP:PORT format, there is no cost to do ResolveTCPAddr here
tcpAddr, err := net.ResolveTCPAddr(network, ipAddr) tcpAddr, err := net.ResolveTCPAddr(network, ipAddr)
if err != nil { if err != nil {

View file

@ -66,7 +66,7 @@ var (
// well as the HTML5 spec: // well as the HTML5 spec:
// http://spec.commonmark.org/0.28/#email-address // http://spec.commonmark.org/0.28/#email-address
// https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail) // https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|\\.(\\s|$))") emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
// blackfriday extensions create IDs like fn:user-content-footnote // blackfriday extensions create IDs like fn:user-content-footnote
blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`) blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)

View file

@ -264,6 +264,18 @@ func TestRender_email(t *testing.T) {
"send email to info@gitea.co.uk.", "send email to info@gitea.co.uk.",
`<p>send email to <a href="mailto:info@gitea.co.uk" rel="nofollow">info@gitea.co.uk</a>.</p>`) `<p>send email to <a href="mailto:info@gitea.co.uk" rel="nofollow">info@gitea.co.uk</a>.</p>`)
test(
`j.doe@example.com,
j.doe@example.com.
j.doe@example.com;
j.doe@example.com?
j.doe@example.com!`,
`<p><a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>,<br/>
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>.<br/>
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>;<br/>
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>?<br/>
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>!</p>`)
// Test that should *not* be turned into email links // Test that should *not* be turned into email links
test( test(
"\"info@gitea.com\"", "\"info@gitea.com\"",

View file

@ -16,6 +16,7 @@ type Package struct {
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
HTMLURL string `json:"html_url"`
// swagger:strfmt date-time // swagger:strfmt date-time
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
} }

View file

@ -180,7 +180,7 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
s := fmt.Sprintf("<span class='ui label scope-parent' title='%s'>"+ s := fmt.Sprintf("<span class='ui label scope-parent' title='%s'>"+
"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+ "<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important''>%s</div>"+ "<div class='ui label scope-right' style='color: %s !important; background-color: %s !important'>%s</div>"+
"</span>", "</span>",
description, description,
textColor, scopeColor, scopeText, textColor, scopeColor, scopeText,

View file

@ -315,10 +315,6 @@ func reqToken() func(ctx *context.APIContext) {
return return
} }
if ctx.IsBasicAuth {
ctx.CheckForOTP()
return
}
if ctx.IsSigned { if ctx.IsSigned {
return return
} }
@ -340,7 +336,6 @@ func reqBasicAuth() func(ctx *context.APIContext) {
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required") ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
return return
} }
ctx.CheckForOTP()
} }
} }
@ -687,12 +682,6 @@ func bind[T any](_ T) any {
} }
} }
// The OAuth2 plugin is expected to be executed first, as it must ignore the user id stored
// in the session (if there is a user id stored in session other plugins might return the user
// object for that id).
//
// The Session plugin is expected to be executed second, in order to skip authentication
// for users that have already signed in.
func buildAuthGroup() *auth.Group { func buildAuthGroup() *auth.Group {
group := auth.NewGroup( group := auth.NewGroup(
&auth.OAuth2{}, &auth.OAuth2{},
@ -1165,8 +1154,8 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Group("/{username}/{reponame}", func() { m.Group("/{username}/{reponame}", func() {
m.Group("/issues", func() { m.Group("/issues", func() {
m.Combo("").Get(repo.ListIssues). m.Combo("").Get(repo.ListIssues).
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue) Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue)
m.Get("/pinned", repo.ListPinnedIssues) m.Get("/pinned", reqRepoReader(unit.TypeIssues), repo.ListPinnedIssues)
m.Group("/comments", func() { m.Group("/comments", func() {
m.Get("", repo.ListRepoIssueComments) m.Get("", repo.ListRepoIssueComments)
m.Group("/{id}", func() { m.Group("/{id}", func() {
@ -1308,10 +1297,10 @@ func Routes(ctx gocontext.Context) *web.Route {
Delete(reqToken(), reqOrgMembership(), org.ConcealMember) Delete(reqToken(), reqOrgMembership(), org.ConcealMember)
}) })
m.Group("/teams", func() { m.Group("/teams", func() {
m.Get("", reqToken(), org.ListTeams) m.Get("", org.ListTeams)
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam) m.Post("", reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
m.Get("/search", reqToken(), org.SearchTeam) m.Get("/search", org.SearchTeam)
}, reqOrgMembership()) }, reqToken(), reqOrgMembership())
m.Group("/labels", func() { m.Group("/labels", func() {
m.Get("", org.ListLabels) m.Get("", org.ListLabels)
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel) m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)

View file

@ -452,6 +452,24 @@ func ListIssues(ctx *context.APIContext) {
isPull = util.OptionalBoolNone isPull = util.OptionalBoolNone
} }
if isPull != util.OptionalBoolNone && !ctx.Repo.CanWriteIssuesOrPulls(isPull.IsTrue()) {
ctx.NotFound()
return
}
if isPull == util.OptionalBoolNone {
canReadIssues := ctx.Repo.CanRead(unit.TypeIssues)
canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests)
if !canReadIssues && !canReadPulls {
ctx.NotFound()
return
} else if !canReadIssues {
isPull = util.OptionalBoolTrue
} else if !canReadPulls {
isPull = util.OptionalBoolFalse
}
}
// FIXME: we should be more efficient here // FIXME: we should be more efficient here
createdByID := getUserIDForFilter(ctx, "created_by") createdByID := getUserIDForFilter(ctx, "created_by")
if ctx.Written() { if ctx.Written() {
@ -562,6 +580,10 @@ func GetIssue(ctx *context.APIContext) {
} }
return return
} }
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
ctx.NotFound()
return
}
ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue)) ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue))
} }

View file

@ -12,9 +12,11 @@ import (
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
@ -69,6 +71,11 @@ func ListIssueComments(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err) ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err)
return return
} }
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
ctx.NotFound()
return
}
issue.Repo = ctx.Repo.Repository issue.Repo = ctx.Repo.Repository
opts := &issues_model.FindCommentsOptions{ opts := &issues_model.FindCommentsOptions{
@ -265,12 +272,27 @@ func ListRepoIssueComments(ctx *context.APIContext) {
return return
} }
var isPull util.OptionalBool
canReadIssue := ctx.Repo.CanRead(unit.TypeIssues)
canReadPull := ctx.Repo.CanRead(unit.TypePullRequests)
if canReadIssue && canReadPull {
isPull = util.OptionalBoolNone
} else if canReadIssue {
isPull = util.OptionalBoolFalse
} else if canReadPull {
isPull = util.OptionalBoolTrue
} else {
ctx.NotFound()
return
}
opts := &issues_model.FindCommentsOptions{ opts := &issues_model.FindCommentsOptions{
ListOptions: utils.GetListOptions(ctx), ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
Type: issues_model.CommentTypeComment, Type: issues_model.CommentTypeComment,
Since: since, Since: since,
Before: before, Before: before,
IsPull: isPull,
} }
comments, err := issues_model.FindComments(ctx, opts) comments, err := issues_model.FindComments(ctx, opts)
@ -357,6 +379,11 @@ func CreateIssueComment(ctx *context.APIContext) {
return return
} }
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
ctx.NotFound()
return
}
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked"))) ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked")))
return return
@ -430,6 +457,11 @@ func GetIssueComment(ctx *context.APIContext) {
return return
} }
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.NotFound()
return
}
if comment.Type != issues_model.CommentTypeComment { if comment.Type != issues_model.CommentTypeComment {
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)
return return
@ -548,7 +580,17 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
return return
} }
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) { if err := comment.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.Status(http.StatusNotFound)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Status(http.StatusForbidden) ctx.Status(http.StatusForbidden)
return return
} }
@ -651,7 +693,17 @@ func deleteIssueComment(ctx *context.APIContext) {
return return
} }
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) { if err := comment.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.Status(http.StatusNotFound)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Status(http.StatusForbidden) ctx.Status(http.StatusForbidden)
return return
} else if comment.Type != issues_model.CommentTypeComment { } else if comment.Type != issues_model.CommentTypeComment {

View file

@ -61,6 +61,12 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
if err := comment.LoadIssue(ctx); err != nil { if err := comment.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err) ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound()
return
} }
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
@ -186,9 +192,19 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
return return
} }
err = comment.LoadIssue(ctx) if err = comment.LoadIssue(ctx); err != nil {
if err != nil {
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue() failed", err) ctx.Error(http.StatusInternalServerError, "comment.LoadIssue() failed", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound()
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.NotFound()
return
} }
if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) { if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) {

View file

@ -155,6 +155,11 @@ func GetDeployKey(ctx *context.APIContext) {
return return
} }
if key.RepoID != ctx.Repo.Repository.ID {
ctx.Status(http.StatusNotFound)
return
}
if err = key.GetContent(); err != nil { if err = key.GetContent(); err != nil {
ctx.Error(http.StatusInternalServerError, "GetContent", err) ctx.Error(http.StatusInternalServerError, "GetContent", err)
return return

View file

@ -19,19 +19,19 @@ import (
"code.gitea.io/gitea/modules/web/routing" "code.gitea.io/gitea/modules/web/routing"
) )
func storageHandler(storageSetting *setting.Storage, prefix string, objStore storage.ObjectStorage) func(next http.Handler) http.Handler { func storageHandler(storageSetting *setting.Storage, prefix string, objStore storage.ObjectStorage) http.HandlerFunc {
prefix = strings.Trim(prefix, "/") prefix = strings.Trim(prefix, "/")
funcInfo := routing.GetFuncInfo(storageHandler, prefix) funcInfo := routing.GetFuncInfo(storageHandler, prefix)
return func(next http.Handler) http.Handler {
if storageSetting.MinioConfig.ServeDirect { if storageSetting.MinioConfig.ServeDirect {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.Method != "GET" && req.Method != "HEAD" { if req.Method != "GET" && req.Method != "HEAD" {
next.ServeHTTP(w, req) http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return return
} }
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") { if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
next.ServeHTTP(w, req) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return return
} }
routing.UpdateFuncInfo(req.Context(), funcInfo) routing.UpdateFuncInfo(req.Context(), funcInfo)
@ -43,7 +43,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
if err != nil { if err != nil {
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) { if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
log.Warn("Unable to find %s %s", prefix, rPath) log.Warn("Unable to find %s %s", prefix, rPath)
http.Error(w, "file not found", http.StatusNotFound) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return return
} }
log.Error("Error whilst getting URL for %s %s. Error: %v", prefix, rPath, err) log.Error("Error whilst getting URL for %s %s. Error: %v", prefix, rPath, err)
@ -57,12 +57,12 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.Method != "GET" && req.Method != "HEAD" { if req.Method != "GET" && req.Method != "HEAD" {
next.ServeHTTP(w, req) http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return return
} }
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") { if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
next.ServeHTTP(w, req) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return return
} }
routing.UpdateFuncInfo(req.Context(), funcInfo) routing.UpdateFuncInfo(req.Context(), funcInfo)
@ -70,7 +70,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/") rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
rPath = util.PathJoinRelX(rPath) rPath = util.PathJoinRelX(rPath)
if rPath == "" || rPath == "." { if rPath == "" || rPath == "." {
http.Error(w, "file not found", http.StatusNotFound) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return return
} }
@ -78,7 +78,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
if err != nil { if err != nil {
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) { if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
log.Warn("Unable to find %s %s", prefix, rPath) log.Warn("Unable to find %s %s", prefix, rPath)
http.Error(w, "file not found", http.StatusNotFound) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return return
} }
log.Error("Error whilst opening %s %s. Error: %v", prefix, rPath, err) log.Error("Error whilst opening %s %s. Error: %v", prefix, rPath, err)
@ -95,5 +95,4 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
defer fr.Close() defer fr.Close()
httpcache.ServeContentWithCacheControl(w, req, path.Base(rPath), fi.ModTime(), fr) httpcache.ServeContentWithCacheControl(w, req, path.Base(rPath), fi.ModTime(), fr)
}) })
}
} }

43
routers/web/githttp.go Normal file
View file

@ -0,0 +1,43 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package web
import (
"net/http"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/repo"
context_service "code.gitea.io/gitea/services/context"
)
func requireSignIn(ctx *context.Context) {
if !setting.Service.RequireSignInView {
return
}
// rely on the results of Contexter
if !ctx.IsSigned {
// TODO: support digit auth - which would be Authorization header with digit
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
ctx.Error(http.StatusUnauthorized)
}
}
func gitHTTPRouters(m *web.Route) {
m.Group("", func() {
m.PostOptions("/git-upload-pack", repo.ServiceUploadPack)
m.PostOptions("/git-receive-pack", repo.ServiceReceivePack)
m.GetOptions("/info/refs", repo.GetInfoRefs)
m.GetOptions("/HEAD", repo.GetTextFile("HEAD"))
m.GetOptions("/objects/info/alternates", repo.GetTextFile("objects/info/alternates"))
m.GetOptions("/objects/info/http-alternates", repo.GetTextFile("objects/info/http-alternates"))
m.GetOptions("/objects/info/packs", repo.GetInfoPacks)
m.GetOptions("/objects/info/{file:[^/]*}", repo.GetTextFile(""))
m.GetOptions("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", repo.GetLooseObject)
m.GetOptions("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", repo.GetPackFile)
m.GetOptions("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", repo.GetIdxFile)
}, ignSignInAndCsrf, requireSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context_service.UserAssignmentWeb())
}

View file

@ -251,7 +251,6 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
isSameRepo = true isSameRepo = true
ci.HeadUser = ctx.Repo.Owner ci.HeadUser = ctx.Repo.Owner
ci.HeadBranch = headInfos[0] ci.HeadBranch = headInfos[0]
} else if len(headInfos) == 2 { } else if len(headInfos) == 2 {
headInfosSplit := strings.Split(headInfos[0], "/") headInfosSplit := strings.Split(headInfos[0], "/")
if len(headInfosSplit) == 1 { if len(headInfosSplit) == 1 {
@ -406,6 +405,9 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
return nil return nil
} }
defer ci.HeadGitRepo.Close() defer ci.HeadGitRepo.Close()
} else {
ctx.NotFound("ParseCompareInfo", nil)
return nil
} }
ctx.Data["HeadRepo"] = ci.HeadRepo ctx.Data["HeadRepo"] = ci.HeadRepo

View file

@ -2971,6 +2971,11 @@ func UpdateCommentContent(ctx *context.Context) {
return return
} }
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Error(http.StatusForbidden) ctx.Error(http.StatusForbidden)
return return
@ -3037,6 +3042,11 @@ func DeleteComment(ctx *context.Context) {
return return
} }
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Error(http.StatusForbidden) ctx.Error(http.StatusForbidden)
return return
@ -3163,6 +3173,11 @@ func ChangeCommentReaction(ctx *context.Context) {
return return
} }
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) { if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
if log.IsTrace() { if log.IsTrace() {
if ctx.IsSigned { if ctx.IsSigned {
@ -3306,6 +3321,16 @@ func GetCommentAttachments(ctx *context.Context) {
return return
} }
if err := comment.LoadIssue(ctx); err != nil {
ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
return
}
if !comment.Type.HasAttachmentSupport() { if !comment.Type.HasAttachmentSupport() {
ctx.ServerError("GetCommentAttachments", fmt.Errorf("comment type %v does not support attachments", comment.Type)) ctx.ServerError("GetCommentAttachments", fmt.Errorf("comment type %v does not support attachments", comment.Type))
return return

View file

@ -125,6 +125,10 @@ func GetContentHistoryDetail(ctx *context.Context) {
}) })
return return
} }
if history.IssueID != issue.ID {
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
return
}
// get the related comment if this history revision is for a comment, otherwise the history revision is for an issue. // get the related comment if this history revision is for a comment, otherwise the history revision is for an issue.
var comment *issues_model.Comment var comment *issues_model.Comment
@ -194,11 +198,19 @@ func SoftDeleteContentHistory(ctx *context.Context) {
log.Error("can not get comment for issue content history %v. err=%v", historyID, err) log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
return return
} }
if comment.IssueID != issue.ID {
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
return
}
} }
if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil { if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil {
log.Error("can not get issue content history %v. err=%v", historyID, err) log.Error("can not get issue content history %v. err=%v", historyID, err)
return return
} }
if history.IssueID != issue.ID {
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
return
}
canSoftDelete := canSoftDeleteContentHistory(ctx, issue, comment, history) canSoftDelete := canSoftDeleteContentHistory(ctx, issue, comment, history)
if !canSoftDelete { if !canSoftDelete {

View file

@ -89,6 +89,10 @@ func IssuePinMove(ctx *context.Context) {
log.Error(err.Error()) log.Error(err.Error())
return return
} }
if issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
return
}
err = issue.MovePin(ctx, form.Position) err = issue.MovePin(ctx, form.Position)
if err != nil { if err != nil {

View file

@ -592,7 +592,17 @@ func DeleteTag(ctx *context.Context) {
} }
func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) { func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
if err := releaseservice.DeleteReleaseByID(ctx, ctx.FormInt64("id"), ctx.Doer, isDelTag); err != nil { id := ctx.FormInt64("id")
rel, err := repo_model.GetReleaseByID(ctx, id)
if err != nil {
ctx.ServerError("GetRelease", err)
return
}
if ctx.Repo.Repository.ID != rel.RepoID {
ctx.NotFound("CompareRepoID", repo_model.ErrReleaseNotExist{})
return
}
if err := releaseservice.DeleteReleaseByID(ctx, id, ctx.Doer, isDelTag); err != nil {
if models.IsErrProtectedTagName(err) { if models.IsErrProtectedTagName(err) {
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected")) ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
} else { } else {

View file

@ -821,6 +821,11 @@ func UsernameSubRoute(ctx *context.Context) {
reloadParam := func(suffix string) (success bool) { reloadParam := func(suffix string) (success bool) {
ctx.SetParams("username", strings.TrimSuffix(username, suffix)) ctx.SetParams("username", strings.TrimSuffix(username, suffix))
context_service.UserAssignmentWeb()(ctx) context_service.UserAssignmentWeb()(ctx)
// check view permissions
if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
ctx.NotFound("user", fmt.Errorf(ctx.ContextUser.Name))
return false
}
return !ctx.Written() return !ctx.Written()
} }
switch { switch {

View file

@ -422,7 +422,7 @@ func PackageSettingsPost(ctx *context.Context) {
redirectURL := ctx.Package.Owner.HomeLink() + "/-/packages" redirectURL := ctx.Package.Owner.HomeLink() + "/-/packages"
// redirect to the package if there are still versions available // redirect to the package if there are still versions available
if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID}); has { if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID, IsInternal: util.OptionalBoolFalse}); has {
redirectURL = ctx.Package.Descriptor.PackageWebLink() redirectURL = ctx.Package.Descriptor.PackageWebLink()
} }

View file

@ -175,6 +175,8 @@ func Routes(ctx gocontext.Context) *web.Route {
return routes return routes
} }
var ignSignInAndCsrf = auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{DisableCSRF: true})
// registerRoutes register routes // registerRoutes register routes
func registerRoutes(m *web.Route) { func registerRoutes(m *web.Route) {
reqSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: true}) reqSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: true})
@ -182,7 +184,6 @@ func registerRoutes(m *web.Route) {
// TODO: rename them to "optSignIn", which means that the "sign-in" could be optional, depends on the VerifyOptions (RequireSignInView) // TODO: rename them to "optSignIn", which means that the "sign-in" could be optional, depends on the VerifyOptions (RequireSignInView)
ignSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: setting.Service.RequireSignInView}) ignSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: setting.Service.RequireSignInView})
ignExploreSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView}) ignExploreSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
ignSignInAndCsrf := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{DisableCSRF: true})
validation.AddBindingRules() validation.AddBindingRules()
linkAccountEnabled := func(ctx *context.Context) { linkAccountEnabled := func(ctx *context.Context) {
@ -1391,19 +1392,7 @@ func registerRoutes(m *web.Route) {
}) })
}, ignSignInAndCsrf, lfsServerEnabled) }, ignSignInAndCsrf, lfsServerEnabled)
m.Group("", func() { gitHTTPRouters(m)
m.PostOptions("/git-upload-pack", repo.ServiceUploadPack)
m.PostOptions("/git-receive-pack", repo.ServiceReceivePack)
m.GetOptions("/info/refs", repo.GetInfoRefs)
m.GetOptions("/HEAD", repo.GetTextFile("HEAD"))
m.GetOptions("/objects/info/alternates", repo.GetTextFile("objects/info/alternates"))
m.GetOptions("/objects/info/http-alternates", repo.GetTextFile("objects/info/http-alternates"))
m.GetOptions("/objects/info/packs", repo.GetInfoPacks)
m.GetOptions("/objects/info/{file:[^/]*}", repo.GetTextFile(""))
m.GetOptions("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", repo.GetLooseObject)
m.GetOptions("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", repo.GetPackFile)
m.GetOptions("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", repo.GetIdxFile)
}, ignSignInAndCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context_service.UserAssignmentWeb())
}) })
}) })
// ***** END: Repository ***** // ***** END: Repository *****

View file

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

View file

@ -85,6 +85,10 @@ func Test_isGitRawOrLFSPath(t *testing.T) {
"/owner/repo/releases/download/tag/repo.tar.gz", "/owner/repo/releases/download/tag/repo.tar.gz",
true, true,
}, },
{
"/owner/repo/attachments/6d92a9ee-5d8b-4993-97c9-6181bdaa8955",
true,
},
} }
lfsTests := []string{ lfsTests := []string{
"/owner/repo/info/lfs/", "/owner/repo/info/lfs/",
@ -104,11 +108,11 @@ func Test_isGitRawOrLFSPath(t *testing.T) {
t.Run(tt.path, func(t *testing.T) { t.Run(tt.path, func(t *testing.T) {
req, _ := http.NewRequest("POST", "http://localhost"+tt.path, nil) req, _ := http.NewRequest("POST", "http://localhost"+tt.path, nil)
setting.LFS.StartServer = false setting.LFS.StartServer = false
if got := isGitRawReleaseOrLFSPath(req); got != tt.want { if got := isGitRawOrAttachOrLFSPath(req); got != tt.want {
t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want) t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want)
} }
setting.LFS.StartServer = true setting.LFS.StartServer = true
if got := isGitRawReleaseOrLFSPath(req); got != tt.want { if got := isGitRawOrAttachOrLFSPath(req); got != tt.want {
t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want) t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want)
} }
}) })
@ -117,11 +121,11 @@ func Test_isGitRawOrLFSPath(t *testing.T) {
t.Run(tt, func(t *testing.T) { t.Run(tt, func(t *testing.T) {
req, _ := http.NewRequest("POST", tt, nil) req, _ := http.NewRequest("POST", tt, nil)
setting.LFS.StartServer = false setting.LFS.StartServer = false
if got := isGitRawReleaseOrLFSPath(req); got != setting.LFS.StartServer { if got := isGitRawOrAttachOrLFSPath(req); got != setting.LFS.StartServer {
t.Errorf("isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, gitRawReleasePathRe.MatchString(tt)) t.Errorf("isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, gitRawOrAttachPathRe.MatchString(tt))
} }
setting.LFS.StartServer = true setting.LFS.StartServer = true
if got := isGitRawReleaseOrLFSPath(req); got != setting.LFS.StartServer { if got := isGitRawOrAttachOrLFSPath(req); got != setting.LFS.StartServer {
t.Errorf("isGitOrLFSPath(%q) = %v, want %v", tt, got, setting.LFS.StartServer) t.Errorf("isGitOrLFSPath(%q) = %v, want %v", tt, got, setting.LFS.StartServer)
} }
}) })

View file

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
) )
@ -43,7 +44,7 @@ func (b *Basic) Name() string {
// Returns nil if header is empty or validation fails. // Returns nil if header is empty or validation fails.
func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
// Basic authentication should only fire on API, Download or on Git or LFSPaths // Basic authentication should only fire on API, Download or on Git or LFSPaths
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawReleaseOrLFSPath(req) { if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) {
return nil, nil return nil, nil
} }
@ -132,11 +133,38 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
return nil, err return nil, err
} }
if skipper, ok := source.Cfg.(LocalTwoFASkipper); ok && skipper.IsSkipLocalTwoFA() { if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
store.GetData()["SkipLocalTwoFA"] = true if err := validateTOTP(req, u); err != nil {
return nil, err
}
} }
log.Trace("Basic Authorization: Logged in user %-v", u) log.Trace("Basic Authorization: Logged in user %-v", u)
return u, nil return u, nil
} }
func getOtpHeader(header http.Header) string {
otpHeader := header.Get("X-Gitea-OTP")
if forgejoHeader := header.Get("X-Forgejo-OTP"); forgejoHeader != "" {
otpHeader = forgejoHeader
}
return otpHeader
}
func validateTOTP(req *http.Request, u *user_model.User) error {
twofa, err := auth_model.GetTwoFactorByUID(u.ID)
if err != nil {
if auth_model.IsErrTwoFactorNotEnrolled(err) {
// No 2FA enrollment for this user
return nil
}
return err
}
if ok, err := twofa.ValidateTOTP(getOtpHeader(req.Header)); err != nil {
return err
} else if !ok {
return util.NewInvalidArgumentErrorf("invalid provided OTP")
}
return nil
}

View file

@ -7,7 +7,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -216,31 +215,6 @@ func VerifyAuthWithOptionsAPI(options *VerifyOptions) func(ctx *context.APIConte
}) })
return return
} }
if ctx.IsSigned && ctx.IsBasicAuth {
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
return // Skip 2FA
}
twofa, err := auth.GetTwoFactorByUID(ctx.Doer.ID)
if err != nil {
if auth.IsErrTwoFactorNotEnrolled(err) {
return // No 2FA enrollment for this user
}
ctx.InternalServerError(err)
return
}
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
ok, err := twofa.ValidateTOTP(otpHeader)
if err != nil {
ctx.InternalServerError(err)
return
}
if !ok {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in user is allowed to call APIs.",
})
return
}
}
} }
if options.AdminRequired { if options.AdminRequired {

View file

@ -128,7 +128,7 @@ func (o *OAuth2) userIDFromToken(tokenSHA string, store DataStore) int64 {
func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
// These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs // These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) && if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) &&
!gitRawReleasePathRe.MatchString(req.URL.Path) { !isGitRawOrAttachPath(req) {
return nil, nil return nil, nil
} }

View file

@ -118,7 +118,7 @@ func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store Da
} }
// Make sure requests to API paths, attachment downloads, git and LFS do not create a new session // Make sure requests to API paths, attachment downloads, git and LFS do not create a new session
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isGitRawReleaseOrLFSPath(req) { if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) {
if sess != nil && (sess.Get("uid") == nil || sess.Get("uid").(int64) != user.ID) { if sess != nil && (sess.Get("uid") == nil || sess.Get("uid").(int64) != user.ID) {
handleSignIn(w, req, sess, user) handleSignIn(w, req, sess, user)
} }

View file

@ -4,10 +4,7 @@
package convert package convert
import ( import (
"strconv"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
) )
@ -16,12 +13,7 @@ func WebAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachm
} }
func APIAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachment) string { func APIAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachment) string {
if attach.CustomDownloadURL != "" { return attach.DownloadURL()
return attach.CustomDownloadURL
}
// /repos/{owner}/{repo}/releases/{id}/assets/{attachment_id}
return setting.AppURL + "api/repos/" + repo.FullName() + "/releases/" + strconv.FormatInt(attach.ReleaseID, 10) + "/assets/" + strconv.FormatInt(attach.ID, 10)
} }
// ToAttachment converts models.Attachment to api.Attachment for API usage // ToAttachment converts models.Attachment to api.Attachment for API usage

View file

@ -35,6 +35,7 @@ func ToPackage(ctx context.Context, pd *packages.PackageDescriptor, doer *user_m
Name: pd.Package.Name, Name: pd.Package.Name,
Version: pd.Version.Version, Version: pd.Version.Version,
CreatedAt: pd.Version.CreatedUnix.AsTime(), CreatedAt: pd.Version.CreatedUnix.AsTime(),
HTMLURL: pd.FullWebLink(),
}, nil }, nil
} }

View file

@ -58,6 +58,10 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
oldTitle := issue.Title oldTitle := issue.Title
issue.Title = title issue.Title = title
if oldTitle == title {
return nil
}
if err = issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil { if err = issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
return return
} }

View file

@ -282,6 +282,8 @@ func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Rele
httpClient := NewMigrationHTTPClient() httpClient := NewMigrationHTTPClient()
for _, asset := range rel.Attachments { for _, asset := range rel.Attachments {
assetID := asset.ID // Don't optimize this, for closure we need a local variable
assetDownloadURL := asset.DownloadURL
size := int(asset.Size) size := int(asset.Size)
dlCount := int(asset.DownloadCount) dlCount := int(asset.DownloadCount)
r.Assets = append(r.Assets, &base.ReleaseAsset{ r.Assets = append(r.Assets, &base.ReleaseAsset{
@ -292,18 +294,18 @@ func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Rele
Created: asset.Created, Created: asset.Created,
DownloadURL: &asset.DownloadURL, DownloadURL: &asset.DownloadURL,
DownloadFunc: func() (io.ReadCloser, error) { DownloadFunc: func() (io.ReadCloser, error) {
asset, _, err := g.client.GetReleaseAttachment(g.repoOwner, g.repoName, rel.ID, asset.ID) asset, _, err := g.client.GetReleaseAttachment(g.repoOwner, g.repoName, rel.ID, assetID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !hasBaseURL(asset.DownloadURL, g.baseURL) { if !hasBaseURL(assetDownloadURL, g.baseURL) {
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.ID, g, asset.DownloadURL) WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", assetID, g, assetDownloadURL)
return io.NopCloser(strings.NewReader(asset.DownloadURL)), nil return io.NopCloser(strings.NewReader(asset.DownloadURL)), nil
} }
// FIXME: for a private download? // FIXME: for a private download?
req, err := http.NewRequest("GET", asset.DownloadURL, nil) req, err := http.NewRequest("GET", assetDownloadURL, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -309,6 +309,7 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea
httpClient := NewMigrationHTTPClient() httpClient := NewMigrationHTTPClient()
for k, asset := range rel.Assets.Links { for k, asset := range rel.Assets.Links {
assetID := asset.ID // Don't optimize this, for closure we need a local variable
r.Assets = append(r.Assets, &base.ReleaseAsset{ r.Assets = append(r.Assets, &base.ReleaseAsset{
ID: int64(asset.ID), ID: int64(asset.ID),
Name: asset.Name, Name: asset.Name,
@ -316,13 +317,13 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea
Size: &zero, Size: &zero,
DownloadCount: &zero, DownloadCount: &zero,
DownloadFunc: func() (io.ReadCloser, error) { DownloadFunc: func() (io.ReadCloser, error) {
link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, rel.TagName, asset.ID, gitlab.WithContext(g.ctx)) link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, rel.TagName, assetID, gitlab.WithContext(g.ctx))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !hasBaseURL(link.URL, g.baseURL) { if !hasBaseURL(link.URL, g.baseURL) {
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.ID, g, link.URL) WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", assetID, g, link.URL)
return io.NopCloser(strings.NewReader(link.URL)), nil return io.NopCloser(strings.NewReader(link.URL)), nil
} }

View file

@ -45,6 +45,9 @@ func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issue
if err := pr.LoadIssue(ctx); err != nil { if err := pr.LoadIssue(ctx); err != nil {
return "", "", err return "", "", err
} }
if err := pr.Issue.LoadPoster(ctx); err != nil {
return "", "", err
}
isExternalTracker := pr.BaseRepo.UnitEnabled(ctx, unit.TypeExternalTracker) isExternalTracker := pr.BaseRepo.UnitEnabled(ctx, unit.TypeExternalTracker)
issueReference := "#" issueReference := "#"

View file

@ -243,7 +243,7 @@ var (
hostMatchers []glob.Glob hostMatchers []glob.Glob
) )
func webhookProxy() func(req *http.Request) (*url.URL, error) { func webhookProxy(allowList *hostmatcher.HostMatchList) func(req *http.Request) (*url.URL, error) {
if setting.Webhook.ProxyURL == "" { if setting.Webhook.ProxyURL == "" {
return proxy.Proxy() return proxy.Proxy()
} }
@ -261,6 +261,9 @@ func webhookProxy() func(req *http.Request) (*url.URL, error) {
return func(req *http.Request) (*url.URL, error) { return func(req *http.Request) (*url.URL, error) {
for _, v := range hostMatchers { for _, v := range hostMatchers {
if v.Match(req.URL.Host) { if v.Match(req.URL.Host) {
if !allowList.MatchHostName(req.URL.Host) {
return nil, fmt.Errorf("webhook can only call allowed HTTP servers (check your %s setting), deny '%s'", allowList.SettingKeyHint, req.URL.Host)
}
return http.ProxyURL(setting.Webhook.ProxyURLFixed)(req) return http.ProxyURL(setting.Webhook.ProxyURLFixed)(req)
} }
} }
@ -282,8 +285,8 @@ func Init() error {
Timeout: timeout, Timeout: timeout,
Transport: &http.Transport{ Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify}, TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify},
Proxy: webhookProxy(), Proxy: webhookProxy(allowedHostMatcher),
DialContext: hostmatcher.NewDialContext("webhook", allowedHostMatcher, nil), DialContext: hostmatcher.NewDialContextWithProxy("webhook", allowedHostMatcher, nil, setting.Webhook.ProxyURLFixed),
}, },
} }

View file

@ -14,35 +14,72 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
webhook_model "code.gitea.io/gitea/models/webhook" webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/hostmatcher"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook" webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestWebhookProxy(t *testing.T) { func TestWebhookProxy(t *testing.T) {
oldWebhook := setting.Webhook
t.Cleanup(func() {
setting.Webhook = oldWebhook
})
setting.Webhook.ProxyURL = "http://localhost:8080" setting.Webhook.ProxyURL = "http://localhost:8080"
setting.Webhook.ProxyURLFixed, _ = url.Parse(setting.Webhook.ProxyURL) setting.Webhook.ProxyURLFixed, _ = url.Parse(setting.Webhook.ProxyURL)
setting.Webhook.ProxyHosts = []string{"*.discordapp.com", "discordapp.com"} setting.Webhook.ProxyHosts = []string{"*.discordapp.com", "discordapp.com"}
kases := map[string]string{ allowedHostMatcher := hostmatcher.ParseHostMatchList("webhook.ALLOWED_HOST_LIST", "discordapp.com,s.discordapp.com")
"https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx": "http://localhost:8080",
"http://s.discordapp.com/assets/xxxxxx": "http://localhost:8080", tests := []struct {
"http://github.com/a/b": "", req string
want string
wantErr bool
}{
{
req: "https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx",
want: "http://localhost:8080",
wantErr: false,
},
{
req: "http://s.discordapp.com/assets/xxxxxx",
want: "http://localhost:8080",
wantErr: false,
},
{
req: "http://github.com/a/b",
want: "",
wantErr: false,
},
{
req: "http://www.discordapp.com/assets/xxxxxx",
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.req, func(t *testing.T) {
req, err := http.NewRequest("POST", tt.req, nil)
require.NoError(t, err)
u, err := webhookProxy(allowedHostMatcher)(req)
if tt.wantErr {
assert.Error(t, err)
return
} }
for reqURL, proxyURL := range kases {
req, err := http.NewRequest("POST", reqURL, nil)
assert.NoError(t, err) assert.NoError(t, err)
u, err := webhookProxy()(req) got := ""
assert.NoError(t, err) if u != nil {
if proxyURL == "" { got = u.String()
assert.Nil(t, u)
} else {
assert.EqualValues(t, proxyURL, u.String())
} }
assert.Equal(t, tt.want, got)
})
} }
} }

View file

@ -173,6 +173,12 @@ func (d *DingtalkPayload) Release(p *api.ReleasePayload) (api.Payloader, error)
return createDingtalkPayload(text, text, "view release", p.Release.HTMLURL), nil return createDingtalkPayload(text, text, "view release", p.Release.HTMLURL), nil
} }
func (d *DingtalkPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
}
func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload { func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload {
return &DingtalkPayload{ return &DingtalkPayload{
MsgType: "actionCard", MsgType: "actionCard",

View file

@ -256,6 +256,12 @@ func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil
} }
func (d *DiscordPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
text, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
}
// GetDiscordPayload converts a discord webhook into a DiscordPayload // GetDiscordPayload converts a discord webhook into a DiscordPayload
func GetDiscordPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) { func GetDiscordPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
s := new(DiscordPayload) s := new(DiscordPayload)

View file

@ -16,7 +16,7 @@ import (
type ( type (
// FeishuPayload represents // FeishuPayload represents
FeishuPayload struct { FeishuPayload struct {
MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
Content struct { Content struct {
Text string `json:"text"` Text string `json:"text"`
} `json:"content"` } `json:"content"`
@ -158,6 +158,12 @@ func (f *FeishuPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
return newFeishuTextPayload(text), nil return newFeishuTextPayload(text), nil
} }
func (f *FeishuPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
return newFeishuTextPayload(text), nil
}
// GetFeishuPayload converts a ding talk webhook into a FeishuPayload // GetFeishuPayload converts a ding talk webhook into a FeishuPayload
func GetFeishuPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) { func GetFeishuPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
return convertPayloader(new(FeishuPayload), p, event) return convertPayloader(new(FeishuPayload), p, event)

View file

@ -230,6 +230,24 @@ func getIssueCommentPayloadInfo(p *api.IssueCommentPayload, linkFormatter linkFo
return text, issueTitle, color return text, issueTitle, color
} }
func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
refLink := linkFormatter(p.Package.HTMLURL, p.Package.Name+":"+p.Package.Version)
switch p.Action {
case api.HookPackageCreated:
text = fmt.Sprintf("Package created: %s", refLink)
color = greenColor
case api.HookPackageDeleted:
text = fmt.Sprintf("Package deleted: %s", refLink)
color = redColor
}
if withSender {
text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
}
return text, color
}
// ToHook convert models.Webhook to api.Hook // ToHook convert models.Webhook to api.Hook
// This function is not part of the convert package to prevent an import cycle // This function is not part of the convert package to prevent an import cycle
func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) { func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {

View file

@ -210,6 +210,21 @@ func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err
return getMatrixPayload(text, nil, m.MsgType), nil return getMatrixPayload(text, nil, m.MsgType), nil
} }
func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
var text string
switch p.Action {
case api.HookPackageCreated:
text = fmt.Sprintf("[%s] Package published by %s", repoLink, senderLink)
case api.HookPackageDeleted:
text = fmt.Sprintf("[%s] Package deleted by %s", repoLink, senderLink)
}
return getMatrixPayload(text, nil, m.MsgType), nil
}
// GetMatrixPayload converts a Matrix webhook into a MatrixPayload // GetMatrixPayload converts a Matrix webhook into a MatrixPayload
func GetMatrixPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) { func GetMatrixPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
s := new(MatrixPayload) s := new(MatrixPayload)

View file

@ -296,6 +296,20 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
), nil ), nil
} }
func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
title, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
return createMSTeamsPayload(
p.Repository,
p.Sender,
title,
"",
p.Package.HTMLURL,
color,
&MSTeamsFact{"Package:", p.Package.Name},
), nil
}
// GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload // GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload
func GetMSTeamsPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) { func GetMSTeamsPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
return convertPayloader(new(MSTeamsPayload), p, event) return convertPayloader(new(MSTeamsPayload), p, event)

View file

@ -104,6 +104,10 @@ func (f *PackagistPayload) Release(_ *api.ReleasePayload) (api.Payloader, error)
return nil, nil return nil, nil
} }
func (f *PackagistPayload) Package(_ *api.PackagePayload) (api.Payloader, error) {
return nil, nil
}
// GetPackagistPayload converts a packagist webhook into a PackagistPayload // GetPackagistPayload converts a packagist webhook into a PackagistPayload
func GetPackagistPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) { func GetPackagistPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
s := new(PackagistPayload) s := new(PackagistPayload)

View file

@ -22,6 +22,7 @@ type PayloadConvertor interface {
Repository(*api.RepositoryPayload) (api.Payloader, error) Repository(*api.RepositoryPayload) (api.Payloader, error)
Release(*api.ReleasePayload) (api.Payloader, error) Release(*api.ReleasePayload) (api.Payloader, error)
Wiki(*api.WikiPayload) (api.Payloader, error) Wiki(*api.WikiPayload) (api.Payloader, error)
Package(*api.PackagePayload) (api.Payloader, error)
} }
func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.HookEventType) (api.Payloader, error) { func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.HookEventType) (api.Payloader, error) {
@ -53,6 +54,8 @@ func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.
return s.Release(p.(*api.ReleasePayload)) return s.Release(p.(*api.ReleasePayload))
case webhook_module.HookEventWiki: case webhook_module.HookEventWiki:
return s.Wiki(p.(*api.WikiPayload)) return s.Wiki(p.(*api.WikiPayload))
case webhook_module.HookEventPackage:
return s.Package(p.(*api.PackagePayload))
} }
return s, nil return s, nil
} }

View file

@ -171,6 +171,12 @@ func (s *SlackPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
return s.createPayload(text, nil), nil return s.createPayload(text, nil), nil
} }
func (s *SlackPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
text, _ := getPackagePayloadInfo(p, SlackLinkFormatter, true)
return s.createPayload(text, nil), nil
}
// Push implements PayloadConvertor Push method // Push implements PayloadConvertor Push method
func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) { func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) {
// n new commits // n new commits

View file

@ -186,6 +186,12 @@ func (t *TelegramPayload) Release(p *api.ReleasePayload) (api.Payloader, error)
return createTelegramPayload(text), nil return createTelegramPayload(text), nil
} }
func (t *TelegramPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
text, _ := getPackagePayloadInfo(p, htmlLinkFormatter, true)
return createTelegramPayload(text), nil
}
// GetTelegramPayload converts a telegram webhook into a TelegramPayload // GetTelegramPayload converts a telegram webhook into a TelegramPayload
func GetTelegramPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) { func GetTelegramPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
return convertPayloader(new(TelegramPayload), p, event) return convertPayloader(new(TelegramPayload), p, event)

View file

@ -179,6 +179,12 @@ func (f *WechatworkPayload) Release(p *api.ReleasePayload) (api.Payloader, error
return newWechatworkMarkdownPayload(text), nil return newWechatworkMarkdownPayload(text), nil
} }
func (f *WechatworkPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
return newWechatworkMarkdownPayload(text), nil
}
// GetWechatworkPayload GetWechatworkPayload converts a ding talk webhook into a WechatworkPayload // GetWechatworkPayload GetWechatworkPayload converts a ding talk webhook into a WechatworkPayload
func GetWechatworkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) { func GetWechatworkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
return convertPayloader(new(WechatworkPayload), p, event) return convertPayloader(new(WechatworkPayload), p, event)

View file

@ -325,8 +325,6 @@
<!-- Environment Config --> <!-- Environment Config -->
<h4 class="ui dividing header">{{.locale.Tr "install.env_config_keys"}}</h4> <h4 class="ui dividing header">{{.locale.Tr "install.env_config_keys"}}</h4>
<div class="inline field"> <div class="inline field">
<label></label>
<button class="ui primary button">{{.locale.Tr "install.install_btn_confirm"}}</button>
<div class="right-content"> <div class="right-content">
{{.locale.Tr "install.env_config_keys_prompt"}} {{.locale.Tr "install.env_config_keys_prompt"}}
</div> </div>

View file

@ -20249,6 +20249,10 @@
"creator": { "creator": {
"$ref": "#/definitions/User" "$ref": "#/definitions/User"
}, },
"html_url": {
"type": "string",
"x-go-name": "HTMLURL"
},
"id": { "id": {
"type": "integer", "type": "integer",
"format": "int64", "format": "int64",

View file

@ -35,6 +35,14 @@ func TestAPIGetCommentAttachment(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
t.Run("UnrelatedCommentID", func(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
MakeRequest(t, req, http.StatusNotFound)
})
session := loginUser(t, repoOwner.Name) session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token) req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)

View file

@ -174,15 +174,29 @@ func TestAPIGetSystemUserComment(t *testing.T) {
} }
func TestAPIEditComment(t *testing.T) { func TestAPIEditComment(t *testing.T) {
defer tests.AddFixtures("tests/integration/fixtures/TestAPIComment/")()
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
const newCommentBody = "This is the new comment body" const newCommentBody = "This is the new comment body"
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{}, comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1008},
unittest.Cond("type = ?", issues_model.CommentTypeComment)) unittest.Cond("type = ?", issues_model.CommentTypeComment))
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
t.Run("UnrelatedCommentID", func(t *testing.T) {
// Using the ID of a comment that does not belong to the repository must fail
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
repoOwner.Name, repo.Name, comment.ID, token)
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
"body": newCommentBody,
})
MakeRequest(t, req, http.StatusNotFound)
})
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue) token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s", urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
repoOwner.Name, repo.Name, comment.ID, token) repoOwner.Name, repo.Name, comment.ID, token)
@ -199,14 +213,25 @@ func TestAPIEditComment(t *testing.T) {
} }
func TestAPIDeleteComment(t *testing.T) { func TestAPIDeleteComment(t *testing.T) {
defer tests.AddFixtures("tests/integration/fixtures/TestAPIComment/")()
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{}, comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1008},
unittest.Cond("type = ?", issues_model.CommentTypeComment)) unittest.Cond("type = ?", issues_model.CommentTypeComment))
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
t.Run("UnrelatedCommentID", func(t *testing.T) {
// Using the ID of a comment that does not belong to the repository must fail
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
repoOwner.Name, repo.Name, comment.ID, token)
MakeRequest(t, req, http.StatusNotFound)
})
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue) token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s", req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
repoOwner.Name, repo.Name, comment.ID, token) repoOwner.Name, repo.Name, comment.ID, token)

View file

@ -12,6 +12,7 @@ import (
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
@ -107,6 +108,27 @@ func TestAPICommentReactions(t *testing.T) {
}) })
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusOK)
t.Run("UnrelatedCommentID", func(t *testing.T) {
// Using the ID of a comment that does not belong to the repository must fail
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/reactions?token=%s",
repoOwner.Name, repo.Name, comment.ID, token)
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
Reaction: "+1",
})
MakeRequest(t, req, http.StatusNotFound)
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
Reaction: "+1",
})
MakeRequest(t, req, http.StatusNotFound)
req = NewRequestf(t, "GET", urlStr)
MakeRequest(t, req, http.StatusNotFound)
})
// Add allowed reaction // Add allowed reaction
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{ req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
Reaction: "+1", Reaction: "+1",

View file

@ -72,6 +72,17 @@ func TestCreateReadOnlyDeployKey(t *testing.T) {
Content: rawKeyBody.Key, Content: rawKeyBody.Key,
Mode: perm.AccessModeRead, Mode: perm.AccessModeRead,
}) })
// Using the ID of a key that does not belong to the repository must fail
{
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/keys/%d?token=%s", repoOwner.Name, repo.Name, newDeployKey.ID, token))
MakeRequest(t, req, http.StatusOK)
session5 := loginUser(t, "user5")
token5 := getTokenForLoggedInUser(t, session5, auth_model.AccessTokenScopeWriteRepository)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user5/repo4/keys/%d?token=%s", newDeployKey.ID, token5))
MakeRequest(t, req, http.StatusNotFound)
}
} }
func TestCreateReadWriteDeployKey(t *testing.T) { func TestCreateReadWriteDeployKey(t *testing.T) {

View file

@ -0,0 +1,59 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
"time"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/tests"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert"
)
func TestAPITwoFactor(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 16})
req := NewRequestf(t, "GET", "/api/v1/user")
req = AddBasicAuthHeader(req, user.Name)
MakeRequest(t, req, http.StatusOK)
otpKey, err := totp.Generate(totp.GenerateOpts{
SecretSize: 40,
Issuer: "gitea-test",
AccountName: user.Name,
})
assert.NoError(t, err)
tfa := &auth_model.TwoFactor{
UID: user.ID,
}
assert.NoError(t, tfa.SetSecret(otpKey.Secret()))
assert.NoError(t, auth_model.NewTwoFactor(tfa))
req = NewRequestf(t, "GET", "/api/v1/user")
req = AddBasicAuthHeader(req, user.Name)
MakeRequest(t, req, http.StatusUnauthorized)
passcode, err := totp.GenerateCode(otpKey.Secret(), time.Now())
assert.NoError(t, err)
req = NewRequestf(t, "GET", "/api/v1/user")
req = AddBasicAuthHeader(req, user.Name)
req.Header.Set("X-Gitea-OTP", passcode)
MakeRequest(t, req, http.StatusOK)
req = NewRequestf(t, "GET", "/api/v1/user")
req = AddBasicAuthHeader(req, user.Name)
req.Header.Set("X-Forgejo-OTP", passcode)
MakeRequest(t, req, http.StatusOK)
}

View file

@ -0,0 +1,9 @@
-
id: 1008
type: 0 # comment
poster_id: 2
issue_id: 4 # in repo_id 2
content: "comment in private pository"
created_unix: 946684811
updated_unix: 946684811

View file

@ -205,6 +205,111 @@ func TestIssueCommentClose(t *testing.T) {
assert.Equal(t, "Description", val) assert.Equal(t, "Description", val)
} }
func TestIssueCommentDelete(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
comment1 := "Test comment 1"
commentID := testIssueAddComment(t, session, issueURL, comment1, "")
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
assert.Equal(t, comment1, comment.Content)
// Using the ID of a comment that does not belong to the repository must fail
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user5", "repo4", commentID), map[string]string{
"_csrf": GetCSRF(t, session, issueURL),
})
session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user2", "repo1", commentID), map[string]string{
"_csrf": GetCSRF(t, session, issueURL),
})
session.MakeRequest(t, req, http.StatusOK)
unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: commentID})
}
func TestIssueCommentAttachment(t *testing.T) {
defer tests.PrepareTestEnv(t)()
const repoURL = "user2/repo1"
const content = "Test comment 4"
const status = ""
session := loginUser(t, "user2")
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
req := NewRequest(t, "GET", issueURL)
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
link, exists := htmlDoc.doc.Find("#comment-form").Attr("action")
assert.True(t, exists, "The template has changed")
uuid := createAttachment(t, session, repoURL, "image.png", generateImg(), http.StatusOK)
commentCount := htmlDoc.doc.Find(".comment-list .comment .render-content").Length()
req = NewRequestWithValues(t, "POST", link, map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"content": content,
"status": status,
"files": uuid,
})
resp = session.MakeRequest(t, req, http.StatusSeeOther)
req = NewRequest(t, "GET", test.RedirectURL(resp))
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
val := htmlDoc.doc.Find(".comment-list .comment .render-content p").Eq(commentCount).Text()
assert.Equal(t, content, val)
idAttr, has := htmlDoc.doc.Find(".comment-list .comment").Eq(commentCount).Attr("id")
idStr := idAttr[strings.LastIndexByte(idAttr, '-')+1:]
assert.True(t, has)
id, err := strconv.Atoi(idStr)
assert.NoError(t, err)
assert.NotEqual(t, 0, id)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/comments/%d/attachments", "user2", "repo1", id))
session.MakeRequest(t, req, http.StatusOK)
// Using the ID of a comment that does not belong to the repository must fail
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/comments/%d/attachments", "user5", "repo4", id))
session.MakeRequest(t, req, http.StatusNotFound)
}
func TestIssueCommentUpdate(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
comment1 := "Test comment 1"
commentID := testIssueAddComment(t, session, issueURL, comment1, "")
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
assert.Equal(t, comment1, comment.Content)
modifiedContent := comment.Content + "MODIFIED"
// Using the ID of a comment that does not belong to the repository must fail
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user5", "repo4", commentID), map[string]string{
"_csrf": GetCSRF(t, session, issueURL),
"content": modifiedContent,
})
session.MakeRequest(t, req, http.StatusNotFound)
commentIdentical := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
assert.Equal(t, comment1, commentIdentical.Content)
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
"_csrf": GetCSRF(t, session, issueURL),
"content": modifiedContent,
})
session.MakeRequest(t, req, http.StatusOK)
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
assert.Equal(t, modifiedContent, comment.Content)
}
func TestIssueReaction(t *testing.T) { func TestIssueReaction(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2") session := loginUser(t, "user2")
@ -556,3 +661,45 @@ func TestUpdateIssueDeadline(t *testing.T) {
assert.EqualValues(t, "2022-04-06", apiIssue.Deadline.Format("2006-01-02")) assert.EqualValues(t, "2022-04-06", apiIssue.Deadline.Format("2006-01-02"))
} }
func TestIssuePinMove(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
issueURL, issue := testIssueWithBean(t, "user2", 1, "Title", "Content")
assert.EqualValues(t, 0, issue.PinOrder)
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/pin", issueURL), map[string]string{
"_csrf": GetCSRF(t, session, issueURL),
})
session.MakeRequest(t, req, http.StatusSeeOther)
issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID})
position := 1
assert.EqualValues(t, position, issue.PinOrder)
newPosition := 2
// Using the ID of an issue that does not belong to the repository must fail
{
session5 := loginUser(t, "user5")
movePinURL := "/user5/repo4/issues/move_pin?_csrf=" + GetCSRF(t, session5, issueURL)
req = NewRequestWithJSON(t, "POST", movePinURL, map[string]any{
"id": issue.ID,
"position": newPosition,
})
session5.MakeRequest(t, req, http.StatusNotFound)
issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID})
assert.EqualValues(t, position, issue.PinOrder)
}
movePinURL := issueURL[:strings.LastIndexByte(issueURL, '/')] + "/move_pin?_csrf=" + GetCSRF(t, session, issueURL)
req = NewRequestWithJSON(t, "POST", movePinURL, map[string]any{
"id": issue.ID,
"position": newPosition,
})
session.MakeRequest(t, req, http.StatusNoContent)
issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID})
assert.EqualValues(t, newPosition, issue.PinOrder)
}

View file

@ -89,6 +89,44 @@ func TestCreateRelease(t *testing.T) {
checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.stable"), 4) checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.stable"), 4)
} }
func TestDeleteRelease(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 57, OwnerName: "user2", LowerName: "repo-release"})
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{TagName: "v2.0"})
assert.False(t, release.IsTag)
// Using the ID of a comment that does not belong to the repository must fail
session5 := loginUser(t, "user5")
otherRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user5", LowerName: "repo4"})
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/releases/delete?id=%d", otherRepo.Link(), release.ID), map[string]string{
"_csrf": GetCSRF(t, session5, otherRepo.Link()),
})
session5.MakeRequest(t, req, http.StatusNotFound)
session := loginUser(t, "user2")
req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/releases/delete?id=%d", repo.Link(), release.ID), map[string]string{
"_csrf": GetCSRF(t, session, repo.Link()),
})
session.MakeRequest(t, req, http.StatusOK)
release = unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: release.ID})
if assert.True(t, release.IsTag) {
req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/tags/delete?id=%d", otherRepo.Link(), release.ID), map[string]string{
"_csrf": GetCSRF(t, session5, otherRepo.Link()),
})
session5.MakeRequest(t, req, http.StatusNotFound)
req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/tags/delete?id=%d", repo.Link(), release.ID), map[string]string{
"_csrf": GetCSRF(t, session, repo.Link()),
})
session.MakeRequest(t, req, http.StatusOK)
unittest.AssertNotExistsBean(t, &repo_model.Release{ID: release.ID})
}
}
func TestCreateReleasePreRelease(t *testing.T) { func TestCreateReleasePreRelease(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()

View file

@ -264,3 +264,13 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() {
func Printf(format string, args ...any) { func Printf(format string, args ...any) {
testlogger.Printf(format, args...) testlogger.Printf(format, args...)
} }
func AddFixtures(dirs ...string) func() {
return unittest.OverrideFixtures(
unittest.FixturesOptions{
Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"),
Base: filepath.Dir(setting.AppPath),
Dirs: dirs,
},
)
}

View file

@ -4,9 +4,11 @@ import {displayError} from './common.js';
const {mermaidMaxSourceCharacters} = window.config; const {mermaidMaxSourceCharacters} = window.config;
// margin removal is for https://github.com/mermaid-js/mermaid/issues/4907
const iframeCss = `:root {color-scheme: normal} const iframeCss = `:root {color-scheme: normal}
body {margin: 0; padding: 0; overflow: hidden} body {margin: 0; padding: 0; overflow: hidden}
#mermaid {display: block; margin: 0 auto}`; #mermaid {display: block; margin: 0 auto}
blockquote, dd, dl, figure, h1, h2, h3, h4, h5, h6, hr, p, pre {margin: 0}`;
export async function renderMermaid() { export async function renderMermaid() {
const els = document.querySelectorAll('.markup code.language-mermaid'); const els = document.querySelectorAll('.markup code.language-mermaid');