From aa8a757fe2d65e6eb9e5bfbfa094e7570d9a67d7 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 5 May 2024 00:05:34 +0000 Subject: [PATCH 1/6] Update module github.com/PuerkitoBio/goquery to v1.9.2 --- go.mod | 8 ++++---- go.sum | 22 ++++++++++------------ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 9dc8ae3f5b..0968b660c7 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 github.com/ProtonMail/go-crypto v1.0.0 - github.com/PuerkitoBio/goquery v1.8.1 + github.com/PuerkitoBio/goquery v1.9.2 github.com/alecthomas/chroma/v2 v2.13.0 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blevesearch/bleve/v2 v2.3.10 @@ -99,11 +99,11 @@ require ( github.com/yuin/goldmark v1.7.0 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-meta v1.1.0 - golang.org/x/crypto v0.21.0 + golang.org/x/crypto v0.22.0 golang.org/x/image v0.15.0 - golang.org/x/net v0.23.0 + golang.org/x/net v0.24.0 golang.org/x/oauth2 v0.16.0 - golang.org/x/sys v0.18.0 + golang.org/x/sys v0.19.0 golang.org/x/text v0.14.0 golang.org/x/tools v0.17.0 google.golang.org/grpc v1.60.1 diff --git a/go.sum b/go.sum index a42bce3812..ba20f72e81 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= -github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= +github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= +github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68= github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= @@ -111,7 +111,6 @@ github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -887,8 +886,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -958,7 +957,6 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -968,8 +966,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1047,8 +1045,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1059,8 +1057,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From d03be7766523904663a6235cb7deea7201c330f2 Mon Sep 17 00:00:00 2001 From: "Panagiotis \"Ivory\" Vasilopoulos" Date: Sun, 5 May 2024 04:16:13 +0200 Subject: [PATCH 2/6] UI: Hide hidden email from own profile, again This is a follow-up for 5e1bd8af5f16f9db88cfeb5b80bdf731435cacfb, which was my first commit to Gitea. It is also a follow up for the Gitea PR #29300 (https://github.com/go-gitea/gitea/pull/23900) created by myself, which turned stale. This change partially restores the behavior of Gitea PR #23747 (https://github.com/go-gitea/gitea/pull/23747) by wxiaoguang, but maintains the lock. The original idea was to differentiate things from GitHub and GitLab a little bit, and show the email address on the profile. The profile is not only a place where the user chooses to show how they present themselves on an instance, it is also a place where they can assess their relationship *with* the instance, as it provides features such as the Public Activity feed that can be only shown to the user, in private. It's, in some way, a dashboard. The email was shown there to remind the user that this is the primary email that will be used by a supposed administrator to contact them. There were other motivations behind that change as well, but, long story short, the idea did not work very well, as some people (e.g. people livestreaming on the Internet, or 'normal' users sharing their screens) do not want to put their email address out there when showing their screen to other people. Other alternatives, such as blurring the text or only showing the real email address, were explored, but were rejected because of browser compatibility and simplicity reasons. The padlock icon that is shown when showing the email address to other people has been kept. One viable alternative could be displaying the placeholder email instead, but that requires some more thought. Fixes https://codeberg.org/forgejo/forgejo/issues/1950. --- options/locale/locale_en-US.ini | 1 - templates/shared/user/profile_big_avatar.tmpl | 29 ++++++------------- tests/integration/setting_test.go | 8 ++--- 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9148d57a27..009be75dd7 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -676,7 +676,6 @@ unblock = Unblock user_bio = Biography disabled_public_activity = This user has disabled the public visibility of the activity. email_visibility.limited = Your email address is visible to all authenticated users -email_visibility.private = Your email address is only visible to you and administrators show_on_map = Show this place on a map settings = User settings diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl index 95277e2f78..a173424eb1 100644 --- a/templates/shared/user/profile_big_avatar.tmpl +++ b/templates/shared/user/profile_big_avatar.tmpl @@ -38,29 +38,18 @@ {{end}} {{end}} - {{if (eq .SignedUserID .ContextUser.ID)}} -
  • - {{svg "octicon-mail"}} - {{.ContextUser.Email}} - - {{if .ShowUserEmail}} - - {{svg "octicon-unlock"}} - - {{else}} - - {{svg "octicon-lock"}} - - {{end}} - -
  • - {{else}} - {{if .ShowUserEmail}} + {{if .ShowUserEmail}}
  • {{svg "octicon-mail"}} - {{.ContextUser.Email}} + {{.ContextUser.Email}} + {{if (eq .SignedUserID .ContextUser.ID)}} + + + {{svg "octicon-unlock"}} + + + {{end}}
  • - {{end}} {{end}} {{if .ContextUser.Website}}
  • diff --git a/tests/integration/setting_test.go b/tests/integration/setting_test.go index 283070ec8e..a82a8afef1 100644 --- a/tests/integration/setting_test.go +++ b/tests/integration/setting_test.go @@ -75,21 +75,21 @@ func TestSettingShowUserEmailProfile(t *testing.T) { htmlDoc = NewHTMLParser(t, resp.Body) assert.Contains(t, htmlDoc.doc.Find(".user.profile").Text(), "user1@example.com") - // user2 can see own hidden email + // user2 cannot see own hidden email session = loginUser(t, "user2") req = NewRequest(t, "GET", "/user2") resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) - assert.Contains(t, htmlDoc.doc.Find(".user.profile").Text(), "user2@example.com") + assert.NotContains(t, htmlDoc.doc.Find(".user.profile").Text(), "user2@example.com") setting.UI.ShowUserEmail = false - // user1 can see own (now hidden) email + // user1 cannot see own (now hidden) email session = loginUser(t, "user1") req = NewRequest(t, "GET", "/user1") resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) - assert.Contains(t, htmlDoc.doc.Find(".user.profile").Text(), "user1@example.com") + assert.NotContains(t, htmlDoc.doc.Find(".user.profile").Text(), "user1@example.com") setting.UI.ShowUserEmail = showUserEmail } From 62c3540467869fa9de6d4ecd5bd897f8457d9039 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 6 May 2024 02:05:34 +0000 Subject: [PATCH 3/6] Update module github.com/cosmtrek/air to v1.52.0 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6d4e3e0703..9496af2cb0 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ DIFF ?= diff --unified XGO_VERSION := go-1.21.x -AIR_PACKAGE ?= github.com/cosmtrek/air@v1.49.0 # renovate: datasource=go +AIR_PACKAGE ?= github.com/cosmtrek/air@v1.52.0 # renovate: datasource=go EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v2/cmd/editorconfig-checker@2.8.0 # renovate: datasource=go GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0 # renovate: datasource=go GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2 # renovate: datasource=go From 2177d38e9c9067fdcc273d68fb6b3549bca6e89d Mon Sep 17 00:00:00 2001 From: Michael Jerger Date: Tue, 7 May 2024 07:59:49 +0000 Subject: [PATCH 4/6] feat(federation): validate like activities (#3494) First step on the way to #1680 The PR will * accept like request on the api * validate activity in a first level You can find * architecture at: https://codeberg.org/meissa/forgejo/src/branch/forgejo-federated-star/docs/unsure-where-to-put/federation-architecture.md Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3494 Reviewed-by: Earl Warren Co-authored-by: Michael Jerger Co-committed-by: Michael Jerger --- .deadcode-out | 11 ++ go.mod | 2 +- modules/forgefed/activity.go | 65 +++++++ modules/forgefed/activity_test.go | 171 ++++++++++++++++++ modules/forgefed/forgefed.go | 49 +++++ modules/forgefed/repository.go | 111 ++++++++++++ modules/forgefed/repository_test.go | 145 +++++++++++++++ modules/validation/validatable.go | 67 +++++++ modules/validation/validatable_test.go | 65 +++++++ routers/api/v1/activitypub/repository.go | 83 +++++++++ routers/api/v1/activitypub/repository_test.go | 27 +++ routers/api/v1/activitypub/response.go | 35 ++++ routers/api/v1/api.go | 9 + routers/api/v1/swagger/options.go | 5 + services/context/repository.go | 25 +++ services/federation/federation_service.go | 30 +++ templates/swagger/v1_json.tmpl | 64 +++++++ .../api_activitypub_repository_test.go | 125 +++++++++++++ 18 files changed, 1088 insertions(+), 1 deletion(-) create mode 100644 modules/forgefed/activity.go create mode 100644 modules/forgefed/activity_test.go create mode 100644 modules/forgefed/forgefed.go create mode 100644 modules/forgefed/repository.go create mode 100644 modules/forgefed/repository_test.go create mode 100644 modules/validation/validatable.go create mode 100644 modules/validation/validatable_test.go create mode 100644 routers/api/v1/activitypub/repository.go create mode 100644 routers/api/v1/activitypub/repository_test.go create mode 100644 routers/api/v1/activitypub/response.go create mode 100644 services/context/repository.go create mode 100644 services/federation/federation_service.go create mode 100644 tests/integration/api_activitypub_repository_test.go diff --git a/.deadcode-out b/.deadcode-out index 62458dd6b6..f22a9df101 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -168,6 +168,14 @@ package "code.gitea.io/gitea/modules/emoji" package "code.gitea.io/gitea/modules/eventsource" func (*Event).String +package "code.gitea.io/gitea/modules/forgefed" + func NewForgeLike + func GetItemByType + func JSONUnmarshalerFn + func NotEmpty + func ToRepository + func OnRepository + package "code.gitea.io/gitea/modules/git" func AllowLFSFiltersArgs func AddChanges @@ -302,6 +310,9 @@ package "code.gitea.io/gitea/modules/translation" package "code.gitea.io/gitea/modules/util/filebuffer" func CreateFromReader +package "code.gitea.io/gitea/modules/validation" + func ValidateMaxLen + package "code.gitea.io/gitea/modules/web" func RouteMock func RouteMockReset diff --git a/go.mod b/go.mod index 73e2d2e7ed..460cc38942 100644 --- a/go.mod +++ b/go.mod @@ -94,6 +94,7 @@ require ( github.com/syndtr/goleveldb v1.0.0 github.com/ulikunitz/xz v0.5.11 github.com/urfave/cli/v2 v2.27.2 + github.com/valyala/fastjson v1.6.4 github.com/xanzy/go-gitlab v0.96.0 github.com/yohcop/openid-go v1.0.1 github.com/yuin/goldmark v1.7.0 @@ -265,7 +266,6 @@ require ( github.com/unknwon/com v1.0.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect - github.com/valyala/fastjson v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect diff --git a/modules/forgefed/activity.go b/modules/forgefed/activity.go new file mode 100644 index 0000000000..c1ca57c4a8 --- /dev/null +++ b/modules/forgefed/activity.go @@ -0,0 +1,65 @@ +// Copyright 2023, 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "time" + + "code.gitea.io/gitea/modules/validation" + + ap "github.com/go-ap/activitypub" +) + +// ForgeLike activity data type +// swagger:model +type ForgeLike struct { + // swagger:ignore + ap.Activity +} + +func NewForgeLike(actorIRI, objectIRI string, startTime time.Time) (ForgeLike, error) { + result := ForgeLike{} + result.Type = ap.LikeType + result.Actor = ap.IRI(actorIRI) // Thats us, a User + result.Object = ap.IRI(objectIRI) // Thats them, a Repository + result.StartTime = startTime + if valid, err := validation.IsValid(result); !valid { + return ForgeLike{}, err + } + return result, nil +} + +func (like ForgeLike) MarshalJSON() ([]byte, error) { + return like.Activity.MarshalJSON() +} + +func (like *ForgeLike) UnmarshalJSON(data []byte) error { + return like.Activity.UnmarshalJSON(data) +} + +func (like ForgeLike) IsNewer(compareTo time.Time) bool { + return like.StartTime.After(compareTo) +} + +func (like ForgeLike) Validate() []string { + var result []string + result = append(result, validation.ValidateNotEmpty(string(like.Type), "type")...) + result = append(result, validation.ValidateOneOf(string(like.Type), []any{"Like"}, "type")...) + if like.Actor == nil { + result = append(result, "Actor should not be nil.") + } else { + result = append(result, validation.ValidateNotEmpty(like.Actor.GetID().String(), "actor")...) + } + if like.Object == nil { + result = append(result, "Object should not be nil.") + } else { + result = append(result, validation.ValidateNotEmpty(like.Object.GetID().String(), "object")...) + } + result = append(result, validation.ValidateNotEmpty(like.StartTime.String(), "startTime")...) + if like.StartTime.IsZero() { + result = append(result, "StartTime was invalid.") + } + + return result +} diff --git a/modules/forgefed/activity_test.go b/modules/forgefed/activity_test.go new file mode 100644 index 0000000000..9a7979c4e6 --- /dev/null +++ b/modules/forgefed/activity_test.go @@ -0,0 +1,171 @@ +// Copyright 2023, 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "fmt" + "reflect" + "strings" + "testing" + "time" + + "code.gitea.io/gitea/modules/validation" + + ap "github.com/go-ap/activitypub" +) + +func Test_NewForgeLike(t *testing.T) { + actorIRI := "https://repo.prod.meissa.de/api/v1/activitypub/user-id/1" + objectIRI := "https://codeberg.org/api/v1/activitypub/repository-id/1" + want := []byte(`{"type":"Like","startTime":"2024-03-27T00:00:00Z","actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1","object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}`) + + startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-27") + sut, err := NewForgeLike(actorIRI, objectIRI, startTime) + if err != nil { + t.Errorf("unexpected error: %v\n", err) + } + if valid, _ := validation.IsValid(sut); !valid { + t.Errorf("sut expected to be valid: %v\n", sut.Validate()) + } + + got, err := sut.MarshalJSON() + if err != nil { + t.Errorf("MarshalJSON() error = \"%v\"", err) + return + } + if !reflect.DeepEqual(got, want) { + t.Errorf("MarshalJSON() got = %q, want %q", got, want) + } +} + +func Test_LikeMarshalJSON(t *testing.T) { + type testPair struct { + item ForgeLike + want []byte + wantErr error + } + + tests := map[string]testPair{ + "empty": { + item: ForgeLike{}, + want: nil, + }, + "with ID": { + item: ForgeLike{ + Activity: ap.Activity{ + Actor: ap.IRI("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"), + Type: "Like", + Object: ap.IRI("https://codeberg.org/api/v1/activitypub/repository-id/1"), + }, + }, + want: []byte(`{"type":"Like","actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1","object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}`), + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + got, err := tt.item.MarshalJSON() + if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() { + t.Errorf("MarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MarshalJSON() got = %q, want %q", got, tt.want) + } + }) + } +} + +func Test_LikeUnmarshalJSON(t *testing.T) { + type testPair struct { + item []byte + want *ForgeLike + wantErr error + } + + //revive:disable + tests := map[string]testPair{ + "with ID": { + item: []byte(`{"type":"Like","actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1","object":"https://codeberg.org/api/activitypub/repository-id/1"}`), + want: &ForgeLike{ + Activity: ap.Activity{ + Actor: ap.IRI("https://repo.prod.meissa.de/api/activitypub/user-id/1"), + Type: "Like", + Object: ap.IRI("https://codeberg.org/api/activitypub/repository-id/1"), + }, + }, + wantErr: nil, + }, + "invalid": { + item: []byte(`{"type":"Invalid","actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1","object":"https://codeberg.org/api/activitypub/repository-id/1"`), + want: &ForgeLike{}, + wantErr: fmt.Errorf("cannot parse JSON:"), + }, + } + //revive:enable + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := new(ForgeLike) + err := got.UnmarshalJSON(test.item) + if (err != nil || test.wantErr != nil) && !strings.Contains(err.Error(), test.wantErr.Error()) { + t.Errorf("UnmarshalJSON() error = \"%v\", wantErr \"%v\"", err, test.wantErr) + return + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("UnmarshalJSON() got = %q, want %q, err %q", got, test.want, err.Error()) + } + }) + } +} + +func TestActivityValidation(t *testing.T) { + sut := new(ForgeLike) + sut.UnmarshalJSON([]byte(`{"type":"Like", + "actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1", + "object":"https://codeberg.org/api/activitypub/repository-id/1", + "startTime": "2014-12-31T23:00:00-08:00"}`)) + if res, _ := validation.IsValid(sut); !res { + t.Errorf("sut expected to be valid: %v\n", sut.Validate()) + } + + sut.UnmarshalJSON([]byte(`{"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1", + "object":"https://codeberg.org/api/activitypub/repository-id/1", + "startTime": "2014-12-31T23:00:00-08:00"}`)) + if sut.Validate()[0] != "type should not be empty" { + t.Errorf("validation error expected but was: %v\n", sut.Validate()[0]) + } + + sut.UnmarshalJSON([]byte(`{"type":"bad-type", + "actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1", + "object":"https://codeberg.org/api/activitypub/repository-id/1", + "startTime": "2014-12-31T23:00:00-08:00"}`)) + if sut.Validate()[0] != "Value bad-type is not contained in allowed values [Like]" { + t.Errorf("validation error expected but was: %v\n", sut.Validate()[0]) + } + + sut.UnmarshalJSON([]byte(`{"type":"Like", + "actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1", + "object":"https://codeberg.org/api/activitypub/repository-id/1", + "startTime": "not a date"}`)) + if sut.Validate()[0] != "StartTime was invalid." { + t.Errorf("validation error expected but was: %v\n", sut.Validate()) + } + + sut.UnmarshalJSON([]byte(`{"type":"Wrong", + "actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1", + "object":"https://codeberg.org/api/activitypub/repository-id/1", + "startTime": "2014-12-31T23:00:00-08:00"}`)) + if sut.Validate()[0] != "Value Wrong is not contained in allowed values [Like]" { + t.Errorf("validation error expected but was: %v\n", sut.Validate()) + } +} + +func TestActivityValidation_Attack(t *testing.T) { + sut := new(ForgeLike) + sut.UnmarshalJSON([]byte(`{rubbish}`)) + if len(sut.Validate()) != 5 { + t.Errorf("5 validateion errors expected but was: %v\n", len(sut.Validate())) + } +} diff --git a/modules/forgefed/forgefed.go b/modules/forgefed/forgefed.go new file mode 100644 index 0000000000..234aecf3ae --- /dev/null +++ b/modules/forgefed/forgefed.go @@ -0,0 +1,49 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + ap "github.com/go-ap/activitypub" + "github.com/valyala/fastjson" +) + +const ForgeFedNamespaceURI = "https://forgefed.org/ns" + +// GetItemByType instantiates a new ForgeFed object if the type matches +// otherwise it defaults to existing activitypub package typer function. +func GetItemByType(typ ap.ActivityVocabularyType) (ap.Item, error) { + switch typ { + case RepositoryType: + return RepositoryNew(""), nil + } + return ap.GetItemByType(typ) +} + +// JSONUnmarshalerFn is the function that will load the data from a fastjson.Value into an Item +// that the go-ap/activitypub package doesn't know about. +func JSONUnmarshalerFn(typ ap.ActivityVocabularyType, val *fastjson.Value, i ap.Item) error { + switch typ { + case RepositoryType: + return OnRepository(i, func(r *Repository) error { + return JSONLoadRepository(val, r) + }) + } + return nil +} + +// NotEmpty is the function that checks if an object is empty +func NotEmpty(i ap.Item) bool { + if ap.IsNil(i) { + return false + } + switch i.GetType() { + case RepositoryType: + r, err := ToRepository(i) + if err != nil { + return false + } + return ap.NotEmpty(r.Actor) + } + return ap.NotEmpty(i) +} diff --git a/modules/forgefed/repository.go b/modules/forgefed/repository.go new file mode 100644 index 0000000000..63680ccd35 --- /dev/null +++ b/modules/forgefed/repository.go @@ -0,0 +1,111 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "reflect" + "unsafe" + + ap "github.com/go-ap/activitypub" + "github.com/valyala/fastjson" +) + +const ( + RepositoryType ap.ActivityVocabularyType = "Repository" +) + +type Repository struct { + ap.Actor + // Team Collection of actors who have management/push access to the repository + Team ap.Item `jsonld:"team,omitempty"` + // Forks OrderedCollection of repositories that are forks of this repository + Forks ap.Item `jsonld:"forks,omitempty"` + // ForkedFrom Identifies the repository which this repository was created as a fork + ForkedFrom ap.Item `jsonld:"forkedFrom,omitempty"` +} + +// RepositoryNew initializes a Repository type actor +func RepositoryNew(id ap.ID) *Repository { + a := ap.ActorNew(id, RepositoryType) + a.Type = RepositoryType + o := Repository{Actor: *a} + return &o +} + +func (r Repository) MarshalJSON() ([]byte, error) { + b, err := r.Actor.MarshalJSON() + if len(b) == 0 || err != nil { + return nil, err + } + + b = b[:len(b)-1] + if r.Team != nil { + ap.JSONWriteItemProp(&b, "team", r.Team) + } + if r.Forks != nil { + ap.JSONWriteItemProp(&b, "forks", r.Forks) + } + if r.ForkedFrom != nil { + ap.JSONWriteItemProp(&b, "forkedFrom", r.ForkedFrom) + } + ap.JSONWrite(&b, '}') + return b, nil +} + +func JSONLoadRepository(val *fastjson.Value, r *Repository) error { + if err := ap.OnActor(&r.Actor, func(a *ap.Actor) error { + return ap.JSONLoadActor(val, a) + }); err != nil { + return err + } + + r.Team = ap.JSONGetItem(val, "team") + r.Forks = ap.JSONGetItem(val, "forks") + r.ForkedFrom = ap.JSONGetItem(val, "forkedFrom") + return nil +} + +func (r *Repository) UnmarshalJSON(data []byte) error { + p := fastjson.Parser{} + val, err := p.ParseBytes(data) + if err != nil { + return err + } + return JSONLoadRepository(val, r) +} + +// ToRepository tries to convert the it Item to a Repository Actor. +func ToRepository(it ap.Item) (*Repository, error) { + switch i := it.(type) { + case *Repository: + return i, nil + case Repository: + return &i, nil + case *ap.Actor: + return (*Repository)(unsafe.Pointer(i)), nil + case ap.Actor: + return (*Repository)(unsafe.Pointer(&i)), nil + default: + // NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes + typ := reflect.TypeOf(new(Repository)) + if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Repository); ok { + return i, nil + } + } + return nil, ap.ErrorInvalidType[ap.Actor](it) +} + +type withRepositoryFn func(*Repository) error + +// OnRepository calls function fn on it Item if it can be asserted to type *Repository +func OnRepository(it ap.Item, fn withRepositoryFn) error { + if it == nil { + return nil + } + ob, err := ToRepository(it) + if err != nil { + return err + } + return fn(ob) +} diff --git a/modules/forgefed/repository_test.go b/modules/forgefed/repository_test.go new file mode 100644 index 0000000000..13a73c10f4 --- /dev/null +++ b/modules/forgefed/repository_test.go @@ -0,0 +1,145 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "fmt" + "reflect" + "testing" + + "code.gitea.io/gitea/modules/json" + + ap "github.com/go-ap/activitypub" +) + +func Test_RepositoryMarshalJSON(t *testing.T) { + type testPair struct { + item Repository + want []byte + wantErr error + } + + tests := map[string]testPair{ + "empty": { + item: Repository{}, + want: nil, + }, + "with ID": { + item: Repository{ + Actor: ap.Actor{ + ID: "https://example.com/1", + }, + Team: nil, + }, + want: []byte(`{"id":"https://example.com/1"}`), + }, + "with Team as IRI": { + item: Repository{ + Team: ap.IRI("https://example.com/1"), + Actor: ap.Actor{ + ID: "https://example.com/1", + }, + }, + want: []byte(`{"id":"https://example.com/1","team":"https://example.com/1"}`), + }, + "with Team as IRIs": { + item: Repository{ + Team: ap.ItemCollection{ + ap.IRI("https://example.com/1"), + ap.IRI("https://example.com/2"), + }, + Actor: ap.Actor{ + ID: "https://example.com/1", + }, + }, + want: []byte(`{"id":"https://example.com/1","team":["https://example.com/1","https://example.com/2"]}`), + }, + "with Team as Object": { + item: Repository{ + Team: ap.Object{ID: "https://example.com/1"}, + Actor: ap.Actor{ + ID: "https://example.com/1", + }, + }, + want: []byte(`{"id":"https://example.com/1","team":{"id":"https://example.com/1"}}`), + }, + "with Team as slice of Objects": { + item: Repository{ + Team: ap.ItemCollection{ + ap.Object{ID: "https://example.com/1"}, + ap.Object{ID: "https://example.com/2"}, + }, + Actor: ap.Actor{ + ID: "https://example.com/1", + }, + }, + want: []byte(`{"id":"https://example.com/1","team":[{"id":"https://example.com/1"},{"id":"https://example.com/2"}]}`), + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + got, err := tt.item.MarshalJSON() + if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() { + t.Errorf("MarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MarshalJSON() got = %q, want %q", got, tt.want) + } + }) + } +} + +func Test_RepositoryUnmarshalJSON(t *testing.T) { + type testPair struct { + data []byte + want *Repository + wantErr error + } + + tests := map[string]testPair{ + "nil": { + data: nil, + wantErr: fmt.Errorf("cannot parse JSON: %w", fmt.Errorf("cannot parse empty string; unparsed tail: %q", "")), + }, + "empty": { + data: []byte{}, + wantErr: fmt.Errorf("cannot parse JSON: %w", fmt.Errorf("cannot parse empty string; unparsed tail: %q", "")), + }, + "with Type": { + data: []byte(`{"type":"Repository"}`), + want: &Repository{ + Actor: ap.Actor{ + Type: RepositoryType, + }, + }, + }, + "with Type and ID": { + data: []byte(`{"id":"https://example.com/1","type":"Repository"}`), + want: &Repository{ + Actor: ap.Actor{ + ID: "https://example.com/1", + Type: RepositoryType, + }, + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + got := new(Repository) + err := got.UnmarshalJSON(tt.data) + if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() { + t.Errorf("UnmarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr) + return + } + if tt.want != nil && !reflect.DeepEqual(got, tt.want) { + jGot, _ := json.Marshal(got) + jWant, _ := json.Marshal(tt.want) + t.Errorf("UnmarshalJSON() got = %s, want %s", jGot, jWant) + } + }) + } +} diff --git a/modules/validation/validatable.go b/modules/validation/validatable.go new file mode 100644 index 0000000000..fc38ad2524 --- /dev/null +++ b/modules/validation/validatable.go @@ -0,0 +1,67 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package validation + +import ( + "fmt" + "strings" + "unicode/utf8" + + "code.gitea.io/gitea/modules/timeutil" +) + +type Validateable interface { + Validate() []string +} + +func IsValid(v Validateable) (bool, error) { + if err := v.Validate(); len(err) > 0 { + errString := strings.Join(err, "\n") + return false, fmt.Errorf(errString) + } + + return true, nil +} + +func ValidateNotEmpty(value any, name string) []string { + isValid := true + switch v := value.(type) { + case string: + if v == "" { + isValid = false + } + case timeutil.TimeStamp: + if v.IsZero() { + isValid = false + } + case int64: + if v == 0 { + isValid = false + } + default: + isValid = false + } + + if isValid { + return []string{} + } + return []string{fmt.Sprintf("%v should not be empty", name)} +} + +func ValidateMaxLen(value string, maxLen int, name string) []string { + if utf8.RuneCountInString(value) > maxLen { + return []string{fmt.Sprintf("Value %v was longer than %v", name, maxLen)} + } + return []string{} +} + +func ValidateOneOf(value any, allowed []any, name string) []string { + for _, allowedElem := range allowed { + if value == allowedElem { + return []string{} + } + } + return []string{fmt.Sprintf("Value %v is not contained in allowed values %v", value, allowed)} +} diff --git a/modules/validation/validatable_test.go b/modules/validation/validatable_test.go new file mode 100644 index 0000000000..fdc21f3223 --- /dev/null +++ b/modules/validation/validatable_test.go @@ -0,0 +1,65 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package validation + +import ( + "testing" + + "code.gitea.io/gitea/modules/timeutil" +) + +type Sut struct { + valid bool +} + +func (sut Sut) Validate() []string { + if sut.valid { + return []string{} + } + return []string{"invalid"} +} + +func Test_IsValid(t *testing.T) { + sut := Sut{valid: true} + if res, _ := IsValid(sut); !res { + t.Errorf("sut expected to be valid: %v\n", sut.Validate()) + } + sut = Sut{valid: false} + if res, _ := IsValid(sut); res { + t.Errorf("sut expected to be invalid: %v\n", sut.Validate()) + } +} + +func Test_ValidateNotEmpty_ForString(t *testing.T) { + sut := "" + if len(ValidateNotEmpty(sut, "dummyField")) == 0 { + t.Errorf("sut should be invalid") + } + sut = "not empty" + if res := ValidateNotEmpty(sut, "dummyField"); len(res) > 0 { + t.Errorf("sut should be valid but was %q", res) + } +} + +func Test_ValidateNotEmpty_ForTimestamp(t *testing.T) { + sut := timeutil.TimeStamp(0) + if res := ValidateNotEmpty(sut, "dummyField"); len(res) == 0 { + t.Errorf("sut should be invalid") + } + sut = timeutil.TimeStampNow() + if res := ValidateNotEmpty(sut, "dummyField"); len(res) > 0 { + t.Errorf("sut should be valid but was %q", res) + } +} + +func Test_ValidateMaxLen(t *testing.T) { + sut := "0123456789" + if len(ValidateMaxLen(sut, 9, "dummyField")) == 0 { + t.Errorf("sut should be invalid") + } + sut = "0123456789" + if res := ValidateMaxLen(sut, 11, "dummyField"); len(res) > 0 { + t.Errorf("sut should be valid but was %q", res) + } +} diff --git a/routers/api/v1/activitypub/repository.go b/routers/api/v1/activitypub/repository.go new file mode 100644 index 0000000000..a9e94f289a --- /dev/null +++ b/routers/api/v1/activitypub/repository.go @@ -0,0 +1,83 @@ +// Copyright 2023, 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package activitypub + +import ( + "fmt" + "net/http" + "strings" + + "code.gitea.io/gitea/modules/forgefed" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/federation" + + ap "github.com/go-ap/activitypub" +) + +// Repository function returns the Repository actor for a repo +func Repository(ctx *context.APIContext) { + // swagger:operation GET /activitypub/repository-id/{repository-id} activitypub activitypubRepository + // --- + // summary: Returns the Repository actor for a repo + // produces: + // - application/json + // parameters: + // - name: repository-id + // in: path + // description: repository ID of the repo + // type: integer + // required: true + // responses: + // "200": + // "$ref": "#/responses/ActivityPub" + + link := fmt.Sprintf("%s/api/v1/activitypub/repository-id/%d", strings.TrimSuffix(setting.AppURL, "/"), ctx.Repo.Repository.ID) + repo := forgefed.RepositoryNew(ap.IRI(link)) + + repo.Name = ap.NaturalLanguageValuesNew() + err := repo.Name.Set("en", ap.Content(ctx.Repo.Repository.Name)) + if err != nil { + ctx.Error(http.StatusInternalServerError, "Set Name", err) + return + } + response(ctx, repo) +} + +// PersonInbox function handles the incoming data for a repository inbox +func RepositoryInbox(ctx *context.APIContext) { + // swagger:operation POST /activitypub/repository-id/{repository-id}/inbox activitypub activitypubRepositoryInbox + // --- + // summary: Send to the inbox + // produces: + // - application/json + // parameters: + // - name: repository-id + // in: path + // description: repository ID of the repo + // type: integer + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/ForgeLike" + // responses: + // "204": + // "$ref": "#/responses/empty" + + repository := ctx.Repo.Repository + log.Info("RepositoryInbox: repo: %v", repository) + + form := web.GetForm(ctx) + httpStatus, title, err := federation.ProcessLikeActivity(ctx, form, repository.ID) + if err != nil { + log.Error("Status: %v", httpStatus) + log.Error("Title: %v", title) + log.Error("Error: %v", err) + ctx.Error(httpStatus, title, err) + } + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/activitypub/repository_test.go b/routers/api/v1/activitypub/repository_test.go new file mode 100644 index 0000000000..acd588d99b --- /dev/null +++ b/routers/api/v1/activitypub/repository_test.go @@ -0,0 +1,27 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package activitypub + +import ( + "testing" + + "code.gitea.io/gitea/models/user" +) + +func Test_UserEmailValidate(t *testing.T) { + sut := "ab@cd.ef" + if err := user.ValidateEmail(sut); err != nil { + t.Errorf("sut should be valid, %v, %v", sut, err) + } + + sut = "83ce13c8-af0b-4112-8327-55a54e54e664@code.cartoon-aa.xyz" + if err := user.ValidateEmail(sut); err != nil { + t.Errorf("sut should be valid, %v, %v", sut, err) + } + + sut = "1" + if err := user.ValidateEmail(sut); err == nil { + t.Errorf("sut should not be valid, %v", sut) + } +} diff --git a/routers/api/v1/activitypub/response.go b/routers/api/v1/activitypub/response.go new file mode 100644 index 0000000000..42ef375f12 --- /dev/null +++ b/routers/api/v1/activitypub/response.go @@ -0,0 +1,35 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package activitypub + +import ( + "net/http" + + "code.gitea.io/gitea/modules/activitypub" + "code.gitea.io/gitea/modules/forgefed" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/services/context" + + ap "github.com/go-ap/activitypub" + "github.com/go-ap/jsonld" +) + +// Respond with an ActivityStreams object +func response(ctx *context.APIContext, v any) { + binary, err := jsonld.WithContext( + jsonld.IRI(ap.ActivityBaseURI), + jsonld.IRI(ap.SecurityContextURI), + jsonld.IRI(forgefed.ForgeFedNamespaceURI), + ).Marshal(v) + if err != nil { + ctx.ServerError("Marshal", err) + return + } + + ctx.Resp.Header().Add("Content-Type", activitypub.ActivityStreamsContentType) + ctx.Resp.WriteHeader(http.StatusOK) + if _, err = ctx.Resp.Write(binary); err != nil { + log.Error("write to resp err: %v", err) + } +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index e4c848cd2f..07b54406c0 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1,5 +1,6 @@ // Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2016 The Gitea Authors. All rights reserved. +// Copyright 2023 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT // Package v1 Gitea API @@ -79,6 +80,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/forgefed" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" @@ -802,6 +804,13 @@ func Routes() *web.Route { m.Get("", activitypub.Person) m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox) }, context.UserIDAssignmentAPI()) + m.Group("/repository-id/{repository-id}", func() { + m.Get("", activitypub.Repository) + m.Post("/inbox", + bind(forgefed.ForgeLike{}), + // TODO: activitypub.ReqHTTPSignature(), + activitypub.RepositoryInbox) + }, context.RepositoryIDAssignmentAPI()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub)) } diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 6a94c21002..2ebf089304 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -1,9 +1,11 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package swagger import ( + ffed "code.gitea.io/gitea/modules/forgefed" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/forms" ) @@ -14,6 +16,9 @@ import ( // parameterBodies // swagger:response parameterBodies type swaggerParameterBodies struct { + // in:body + ForgeLike ffed.ForgeLike + // in:body AddCollaboratorOption api.AddCollaboratorOption diff --git a/services/context/repository.go b/services/context/repository.go new file mode 100644 index 0000000000..422ac3f58d --- /dev/null +++ b/services/context/repository.go @@ -0,0 +1,25 @@ +// Copyright 2023, 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "net/http" + + repo_model "code.gitea.io/gitea/models/repo" +) + +// RepositoryIDAssignmentAPI returns a middleware to handle context-repo assignment for api routes +func RepositoryIDAssignmentAPI() func(ctx *APIContext) { + return func(ctx *APIContext) { + repositoryID := ctx.ParamsInt64(":repository-id") + + var err error + repository := new(Repository) + repository.Repository, err = repo_model.GetRepositoryByID(ctx, repositoryID) + if err != nil { + ctx.Error(http.StatusNotFound, "GetRepositoryByID", err) + } + ctx.Repo = repository + } +} diff --git a/services/federation/federation_service.go b/services/federation/federation_service.go new file mode 100644 index 0000000000..478b00df96 --- /dev/null +++ b/services/federation/federation_service.go @@ -0,0 +1,30 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package federation + +import ( + "context" + "net/http" + + fm "code.gitea.io/gitea/modules/forgefed" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/validation" +) + +// ProcessLikeActivity receives a ForgeLike activity and does the following: +// Validation of the activity +// Creation of a (remote) federationHost if not existing +// Creation of a forgefed Person if not existing +// Validation of incoming RepositoryID against Local RepositoryID +// Star the repo if it wasn't already stared +// Do some mitigation against out of order attacks +func ProcessLikeActivity(ctx context.Context, form any, repositoryID int64) (int, string, error) { + activity := form.(*fm.ForgeLike) + if res, err := validation.IsValid(activity); !res { + return http.StatusNotAcceptable, "Invalid activity", err + } + log.Info("Activity validated:%v", activity) + + return 0, "", nil +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index dbf9eb89e2..0137e05434 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -23,6 +23,65 @@ }, "basePath": "{{AppSubUrl | JSEscape}}/api/v1", "paths": { + "/activitypub/repository-id/{repository-id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "activitypub" + ], + "summary": "Returns the Repository actor for a repo", + "operationId": "activitypubRepository", + "parameters": [ + { + "type": "integer", + "description": "repository ID of the repo", + "name": "repository-id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ActivityPub" + } + } + } + }, + "/activitypub/repository-id/{repository-id}/inbox": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "activitypub" + ], + "summary": "Send to the inbox", + "operationId": "activitypubRepositoryInbox", + "parameters": [ + { + "type": "integer", + "description": "repository ID of the repo", + "name": "repository-id", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/ForgeLike" + } + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + } + } + } + }, "/activitypub/user-id/{user-id}": { "get": { "produces": [ @@ -21373,6 +21432,11 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "ForgeLike": { + "description": "ForgeLike activity data type", + "type": "object", + "x-go-package": "code.gitea.io/gitea/modules/forgefed" + }, "GPGKey": { "description": "GPGKey a user GPG key to sign commit and tag in repository", "type": "object", diff --git a/tests/integration/api_activitypub_repository_test.go b/tests/integration/api_activitypub_repository_test.go new file mode 100644 index 0000000000..19bf1cf094 --- /dev/null +++ b/tests/integration/api_activitypub_repository_test.go @@ -0,0 +1,125 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/activitypub" + forgefed_modules "code.gitea.io/gitea/modules/forgefed" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/routers" + + "github.com/stretchr/testify/assert" +) + +func TestActivityPubRepository(t *testing.T) { + setting.Federation.Enabled = true + testWebRoutes = routers.NormalRoutes() + defer func() { + setting.Federation.Enabled = false + testWebRoutes = routers.NormalRoutes() + }() + + onGiteaRun(t, func(*testing.T, *url.URL) { + repositoryID := 2 + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/activitypub/repository-id/%v", repositoryID)) + resp := MakeRequest(t, req, http.StatusOK) + body := resp.Body.Bytes() + assert.Contains(t, string(body), "@context") + + var repository forgefed_modules.Repository + err := repository.UnmarshalJSON(body) + assert.NoError(t, err) + + assert.Regexp(t, fmt.Sprintf("activitypub/repository-id/%v$", repositoryID), repository.GetID().String()) + }) +} + +func TestActivityPubMissingRepository(t *testing.T) { + setting.Federation.Enabled = true + testWebRoutes = routers.NormalRoutes() + defer func() { + setting.Federation.Enabled = false + testWebRoutes = routers.NormalRoutes() + }() + + onGiteaRun(t, func(*testing.T, *url.URL) { + repositoryID := 9999999 + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/activitypub/repository-id/%v", repositoryID)) + resp := MakeRequest(t, req, http.StatusNotFound) + assert.Contains(t, resp.Body.String(), "repository does not exist") + }) +} + +func TestActivityPubRepositoryInboxValid(t *testing.T) { + setting.Federation.Enabled = true + testWebRoutes = routers.NormalRoutes() + defer func() { + setting.Federation.Enabled = false + testWebRoutes = routers.NormalRoutes() + }() + + srv := httptest.NewServer(testWebRoutes) + defer srv.Close() + + onGiteaRun(t, func(*testing.T, *url.URL) { + appURL := setting.AppURL + setting.AppURL = srv.URL + "/" + defer func() { + setting.Database.LogSQL = false + setting.AppURL = appURL + }() + actionsUser := user.NewActionsUser() + repositoryID := 2 + c, err := activitypub.NewClient(db.DefaultContext, actionsUser, "not used") + assert.NoError(t, err) + repoInboxURL := fmt.Sprintf("%s/api/v1/activitypub/repository-id/%v/inbox", + srv.URL, repositoryID) + + activity := []byte(fmt.Sprintf(`{"type":"Like","startTime":"2024-03-27T00:00:00Z","actor":"%s/api/v1/activitypub/user-id/2","object":"%s/api/v1/activitypub/repository-id/%v"}`, + srv.URL, srv.URL, repositoryID)) + resp, err := c.Post(activity, repoInboxURL) + assert.NoError(t, err) + assert.Equal(t, http.StatusNoContent, resp.StatusCode) + }) +} + +func TestActivityPubRepositoryInboxInvalid(t *testing.T) { + setting.Federation.Enabled = true + testWebRoutes = routers.NormalRoutes() + defer func() { + setting.Federation.Enabled = false + testWebRoutes = routers.NormalRoutes() + }() + + srv := httptest.NewServer(testWebRoutes) + defer srv.Close() + + onGiteaRun(t, func(*testing.T, *url.URL) { + appURL := setting.AppURL + setting.AppURL = srv.URL + "/" + defer func() { + setting.Database.LogSQL = false + setting.AppURL = appURL + }() + actionsUser := user.NewActionsUser() + repositoryID := 2 + c, err := activitypub.NewClient(db.DefaultContext, actionsUser, "not used") + assert.NoError(t, err) + repoInboxURL := fmt.Sprintf("%s/api/v1/activitypub/repository-id/%v/inbox", + srv.URL, repositoryID) + + activity := []byte(`{"type":"Wrong"}`) + resp, err := c.Post(activity, repoInboxURL) + assert.NoError(t, err) + assert.Equal(t, http.StatusNotAcceptable, resp.StatusCode) + }) +} From 44895011dc6fe36a2719e25e105912a3b281b5bb Mon Sep 17 00:00:00 2001 From: Twenty Panda Date: Tue, 7 May 2024 10:11:21 +0100 Subject: [PATCH 5/6] chore(dependencies): switch to air@v1 there are no tests but since Gitea uses @v1 since last month and Gitea maintainers rely on make watch, it is safe to assume that upgrading is not broken. Switching to v1 would require less scrutiny on the upgrades. Even if there is breakage, it can be fixed with minimal impact on the developer workflow. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9496af2cb0..f882fdd026 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ DIFF ?= diff --unified XGO_VERSION := go-1.21.x -AIR_PACKAGE ?= github.com/cosmtrek/air@v1.52.0 # renovate: datasource=go +AIR_PACKAGE ?= github.com/cosmtrek/air@v1 # renovate: datasource=go EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v2/cmd/editorconfig-checker@2.8.0 # renovate: datasource=go GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0 # renovate: datasource=go GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2 # renovate: datasource=go From cae4a5456fb9d275b6e1b1cb146371a1176d2d55 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Tue, 7 May 2024 11:46:10 +0100 Subject: [PATCH 6/6] chore(dependency): automerge goquery when the CI passes --- renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index 0a6bf6f700..b8d0fae32c 100644 --- a/renovate.json +++ b/renovate.json @@ -97,7 +97,7 @@ { "description": "Automerge some packages when ci succeeds", "extends": ["packages:linters"], - "matchDepNames": ["vitest", "vite-string-plugin"], + "matchDepNames": ["github.com/PuerkitoBio/goquery", "vitest", "vite-string-plugin"], "automerge": true }, {