Skip to content

Commit 5780e1a

Browse files
committed
Merge remote-tracking branch 'upstream/main'
* upstream/main: Fix edit topic UI (go-gitea#27925) Unify two factor check (go-gitea#27915) Revert go-gitea#27870 (go-gitea#27917) Fix JS NPE when viewing specific range of PR commits (go-gitea#27912) Install poetry dependencies with --no-root (go-gitea#27919) Show correct commit sha when viewing single commit diff (go-gitea#27916) Fix 500 when deleting a dismissed review (go-gitea#27903) Remove action runners on user deletion (go-gitea#27902) Remove SSH workaround (go-gitea#27893) Remove "tabindex" from some form buttons (go-gitea#27892) Refactor the function RemoveOrgUser (go-gitea#27582) Fix DownloadFunc when migrating releases (go-gitea#27887)
2 parents 23448c7 + 7a2ff6c commit 5780e1a

File tree

23 files changed

+191
-120
lines changed

23 files changed

+191
-120
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,7 @@ node_modules: package-lock.json
875875
@touch node_modules
876876

877877
.venv: poetry.lock
878-
poetry install
878+
poetry install --no-root
879879
@touch .venv
880880

881881
.PHONY: update

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ require (
3636
github.com/ethantkoenig/rupture v1.0.1
3737
github.com/felixge/fgprof v0.9.3
3838
github.com/fsnotify/fsnotify v1.6.0
39-
github.com/gliderlabs/ssh v0.3.5
39+
github.com/gliderlabs/ssh v0.3.6-0.20230927171611-ece6c7995e46
4040
github.com/go-ap/activitypub v0.0.0-20231003111253-1fba3772399b
4141
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
4242
github.com/go-chi/chi/v5 v5.0.10

go.sum

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,8 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
329329
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
330330
github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
331331
github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
332-
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
333-
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
332+
github.com/gliderlabs/ssh v0.3.6-0.20230927171611-ece6c7995e46 h1:fYiA820jw7wmAvdXrHwMItxjJkra7dT9y8yiXhtzb94=
333+
github.com/gliderlabs/ssh v0.3.6-0.20230927171611-ece6c7995e46/go.mod h1:i/TCLcdiX9Up/vs+Rp8c3yMbqp2Y4Y7Nh9uzGFCa5pM=
334334
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
335335
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
336336
github.com/go-ap/activitypub v0.0.0-20231003111253-1fba3772399b h1:VLD6IPBDkqEsOZ+EfLO6MayuHycZ0cv4BStTlRoZduo=
@@ -1237,7 +1237,6 @@ golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qx
12371237
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
12381238
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
12391239
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
1240-
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
12411240
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
12421241
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
12431242
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@@ -1337,9 +1336,7 @@ golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBc
13371336
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13381337
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13391338
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1340-
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13411339
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1342-
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13431340
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13441341
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13451342
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1353,7 +1350,6 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13531350
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
13541351
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
13551352
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
1356-
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
13571353
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
13581354
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
13591355
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=

models/actions/runner.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,27 @@ func CreateRunner(ctx context.Context, t *ActionRunner) error {
266266
_, err := db.GetEngine(ctx).Insert(t)
267267
return err
268268
}
269+
270+
func CountRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) {
271+
// Only affect action runners were a owner ID is set, as actions runners
272+
// could also be created on a repository.
273+
return db.GetEngine(ctx).Table("action_runner").
274+
Join("LEFT", "user", "`action_runner`.owner_id = `user`.id").
275+
Where("`action_runner`.owner_id != ?", 0).
276+
And(builder.IsNull{"`user`.id"}).
277+
Count(new(ActionRunner))
278+
}
279+
280+
func FixRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) {
281+
subQuery := builder.Select("`action_runner`.id").
282+
From("`action_runner`").
283+
Join("LEFT", "user", "`action_runner`.owner_id = `user`.id").
284+
Where(builder.Neq{"`action_runner`.owner_id": 0}).
285+
And(builder.IsNull{"`user`.id"})
286+
b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`")
287+
res, err := db.GetEngine(ctx).Exec(b)
288+
if err != nil {
289+
return 0, err
290+
}
291+
return res.RowsAffected()
292+
}

models/issues/review.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,16 @@ func DeleteReview(ctx context.Context, r *Review) error {
897897
return err
898898
}
899899

900+
opts = FindCommentsOptions{
901+
Type: CommentTypeDismissReview,
902+
IssueID: r.IssueID,
903+
ReviewID: r.ID,
904+
}
905+
906+
if _, err := sess.Where(opts.ToConds()).Delete(new(Comment)); err != nil {
907+
return err
908+
}
909+
900910
if _, err := sess.ID(r.ID).Delete(new(Review)); err != nil {
901911
return err
902912
}

models/issues/review_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"code.gitea.io/gitea/models/db"
1010
issues_model "code.gitea.io/gitea/models/issues"
11+
repo_model "code.gitea.io/gitea/models/repo"
1112
"code.gitea.io/gitea/models/unittest"
1213
user_model "code.gitea.io/gitea/models/user"
1314

@@ -258,3 +259,32 @@ func TestDeleteReview(t *testing.T) {
258259
assert.NoError(t, err)
259260
assert.True(t, review1.Official)
260261
}
262+
263+
func TestDeleteDismissedReview(t *testing.T) {
264+
assert.NoError(t, unittest.PrepareTestDatabase())
265+
266+
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
267+
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
268+
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
269+
review, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
270+
Content: "reject",
271+
Type: issues_model.ReviewTypeReject,
272+
Official: false,
273+
Issue: issue,
274+
Reviewer: user,
275+
})
276+
assert.NoError(t, err)
277+
assert.NoError(t, issues_model.DismissReview(db.DefaultContext, review, true))
278+
comment, err := issues_model.CreateComment(db.DefaultContext, &issues_model.CreateCommentOptions{
279+
Type: issues_model.CommentTypeDismissReview,
280+
Doer: user,
281+
Repo: repo,
282+
Issue: issue,
283+
ReviewID: review.ID,
284+
Content: "dismiss",
285+
})
286+
assert.NoError(t, err)
287+
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID})
288+
assert.NoError(t, issues_model.DeleteReview(db.DefaultContext, review))
289+
unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID})
290+
}

models/org.go

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@ import (
1414
repo_model "code.gitea.io/gitea/models/repo"
1515
)
1616

17-
func removeOrgUser(ctx context.Context, orgID, userID int64) error {
17+
// RemoveOrgUser removes user from given organization.
18+
func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
1819
ou := new(organization.OrgUser)
1920

20-
sess := db.GetEngine(ctx)
21-
22-
has, err := sess.
21+
has, err := db.GetEngine(ctx).
2322
Where("uid=?", userID).
2423
And("org_id=?", orgID).
2524
Get(ou)
@@ -52,7 +51,13 @@ func removeOrgUser(ctx context.Context, orgID, userID int64) error {
5251
}
5352
}
5453

55-
if _, err := sess.ID(ou.ID).Delete(ou); err != nil {
54+
ctx, committer, err := db.TxContext(ctx)
55+
if err != nil {
56+
return err
57+
}
58+
defer committer.Close()
59+
60+
if _, err := db.GetEngine(ctx).ID(ou.ID).Delete(ou); err != nil {
5661
return err
5762
} else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", orgID); err != nil {
5863
return err
@@ -74,7 +79,7 @@ func removeOrgUser(ctx context.Context, orgID, userID int64) error {
7479
}
7580

7681
if len(repoIDs) > 0 {
77-
if _, err = sess.
82+
if _, err = db.GetEngine(ctx).
7883
Where("user_id = ?", userID).
7984
In("repo_id", repoIDs).
8085
Delete(new(access_model.Access)); err != nil {
@@ -93,18 +98,5 @@ func removeOrgUser(ctx context.Context, orgID, userID int64) error {
9398
}
9499
}
95100

96-
return nil
97-
}
98-
99-
// RemoveOrgUser removes user from given organization.
100-
func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
101-
ctx, committer, err := db.TxContext(ctx)
102-
if err != nil {
103-
return err
104-
}
105-
defer committer.Close()
106-
if err := removeOrgUser(ctx, orgID, userID); err != nil {
107-
return err
108-
}
109101
return committer.Commit()
110102
}

models/org_team.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ func removeInvalidOrgUser(ctx context.Context, userID, orgID int64) error {
502502
}); err != nil {
503503
return err
504504
} else if count == 0 {
505-
return removeOrgUser(ctx, orgID, userID)
505+
return RemoveOrgUser(ctx, orgID, userID)
506506
}
507507
return nil
508508
}

modules/context/api.go

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"net/url"
1212
"strings"
1313

14-
"code.gitea.io/gitea/models/auth"
1514
repo_model "code.gitea.io/gitea/models/repo"
1615
"code.gitea.io/gitea/models/unit"
1716
user_model "code.gitea.io/gitea/models/user"
@@ -211,32 +210,6 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
211210
}
212211
}
213212

214-
// CheckForOTP validates OTP
215-
func (ctx *APIContext) CheckForOTP() {
216-
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
217-
return // Skip 2FA
218-
}
219-
220-
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
221-
twofa, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
222-
if err != nil {
223-
if auth.IsErrTwoFactorNotEnrolled(err) {
224-
return // No 2FA enrollment for this user
225-
}
226-
ctx.Error(http.StatusInternalServerError, "GetTwoFactorByUID", err)
227-
return
228-
}
229-
ok, err := twofa.ValidateTOTP(otpHeader)
230-
if err != nil {
231-
ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err)
232-
return
233-
}
234-
if !ok {
235-
ctx.Error(http.StatusUnauthorized, "", nil)
236-
return
237-
}
238-
}
239-
240213
// APIContexter returns apicontext as middleware
241214
func APIContexter() func(http.Handler) http.Handler {
242215
return func(next http.Handler) http.Handler {

modules/doctor/dbconsistency.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package doctor
66
import (
77
"context"
88

9+
actions_model "code.gitea.io/gitea/models/actions"
910
activities_model "code.gitea.io/gitea/models/activities"
1011
"code.gitea.io/gitea/models/db"
1112
issues_model "code.gitea.io/gitea/models/issues"
@@ -151,6 +152,12 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
151152
Fixer: activities_model.FixActionCreatedUnixString,
152153
FixedMessage: "Set to zero",
153154
},
155+
{
156+
Name: "Action Runners without existing owner",
157+
Counter: actions_model.CountRunnersWithoutBelongingOwner,
158+
Fixer: actions_model.FixRunnersWithoutBelongingOwner,
159+
FixedMessage: "Removed",
160+
},
154161
}
155162

156163
// TODO: function to recalc all counters

modules/ssh/ssh.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"os"
1818
"os/exec"
1919
"path/filepath"
20-
"reflect"
2120
"strconv"
2221
"strings"
2322
"sync"
@@ -165,10 +164,6 @@ func sessionHandler(session ssh.Session) {
165164
}
166165

167166
func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
168-
// FIXME: the "ssh.Context" is not thread-safe, so db operations should use the immutable parent "Context"
169-
// TODO: Remove after https://github.com/gliderlabs/ssh/pull/211
170-
parentCtx := reflect.ValueOf(ctx).Elem().FieldByName("Context").Interface().(context.Context)
171-
172167
if log.IsDebug() { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary
173168
log.Debug("Handle Public Key: Fingerprint: %s from %s", gossh.FingerprintSHA256(key), ctx.RemoteAddr())
174169
}
@@ -200,7 +195,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
200195
// look for the exact principal
201196
principalLoop:
202197
for _, principal := range cert.ValidPrincipals {
203-
pkey, err := asymkey_model.SearchPublicKeyByContentExact(parentCtx, principal)
198+
pkey, err := asymkey_model.SearchPublicKeyByContentExact(ctx, principal)
204199
if err != nil {
205200
if asymkey_model.IsErrKeyNotExist(err) {
206201
log.Debug("Principal Rejected: %s Unknown Principal: %s", ctx.RemoteAddr(), principal)
@@ -257,7 +252,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
257252
log.Debug("Handle Public Key: %s Fingerprint: %s is not a certificate", ctx.RemoteAddr(), gossh.FingerprintSHA256(key))
258253
}
259254

260-
pkey, err := asymkey_model.SearchPublicKeyByContent(parentCtx, strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key))))
255+
pkey, err := asymkey_model.SearchPublicKeyByContent(ctx, strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key))))
261256
if err != nil {
262257
if asymkey_model.IsErrKeyNotExist(err) {
263258
log.Warn("Unknown public key: %s from %s", gossh.FingerprintSHA256(key), ctx.RemoteAddr())

routers/api/v1/api.go

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,6 @@ func reqToken() func(ctx *context.APIContext) {
316316
return
317317
}
318318

319-
if ctx.IsBasicAuth {
320-
ctx.CheckForOTP()
321-
return
322-
}
323319
if ctx.IsSigned {
324320
return
325321
}
@@ -344,7 +340,6 @@ func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) {
344340
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
345341
return
346342
}
347-
ctx.CheckForOTP()
348343
}
349344
}
350345

@@ -701,12 +696,6 @@ func bind[T any](_ T) any {
701696
}
702697
}
703698

704-
// The OAuth2 plugin is expected to be executed first, as it must ignore the user id stored
705-
// in the session (if there is a user id stored in session other plugins might return the user
706-
// object for that id).
707-
//
708-
// The Session plugin is expected to be executed second, in order to skip authentication
709-
// for users that have already signed in.
710699
func buildAuthGroup() *auth.Group {
711700
group := auth.NewGroup(
712701
&auth.OAuth2{},
@@ -786,31 +775,6 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC
786775
})
787776
return
788777
}
789-
if ctx.IsSigned && ctx.IsBasicAuth {
790-
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
791-
return // Skip 2FA
792-
}
793-
twofa, err := auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID)
794-
if err != nil {
795-
if auth_model.IsErrTwoFactorNotEnrolled(err) {
796-
return // No 2FA enrollment for this user
797-
}
798-
ctx.InternalServerError(err)
799-
return
800-
}
801-
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
802-
ok, err := twofa.ValidateTOTP(otpHeader)
803-
if err != nil {
804-
ctx.InternalServerError(err)
805-
return
806-
}
807-
if !ok {
808-
ctx.JSON(http.StatusForbidden, map[string]string{
809-
"message": "Only signed in user is allowed to call APIs.",
810-
})
811-
return
812-
}
813-
}
814778
}
815779

816780
if options.AdminRequired {

routers/web/user/setting/security/security.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func loadSecurityData(ctx *context.Context) {
8484
// map the provider display name with the AuthSource
8585
sources := make(map[*auth_model.Source]string)
8686
for _, externalAccount := range accountLinks {
87-
if authSource, err := auth_model.GetSourceByID(ctx, externalAccount.LoginSourceID); err == nil && authSource.IsActive {
87+
if authSource, err := auth_model.GetSourceByID(ctx, externalAccount.LoginSourceID); err == nil {
8888
var providerDisplayName string
8989

9090
type DisplayNamed interface {

0 commit comments

Comments
 (0)