1
1
// Copyright 2016 The Gogs Authors. All rights reserved.
2
+ // Copyright 2020 The Gitea Authors. All rights reserved.
2
3
// Use of this source code is governed by a MIT-style
3
4
// license that can be found in the LICENSE file.
4
5
@@ -8,6 +9,12 @@ import (
8
9
"errors"
9
10
"fmt"
10
11
"strings"
12
+
13
+ "code.gitea.io/gitea/modules/log"
14
+ "code.gitea.io/gitea/modules/setting"
15
+ "code.gitea.io/gitea/modules/util"
16
+
17
+ "xorm.io/builder"
11
18
)
12
19
13
20
var (
@@ -54,13 +61,66 @@ func GetEmailAddresses(uid int64) ([]*EmailAddress, error) {
54
61
if ! isPrimaryFound {
55
62
emails = append (emails , & EmailAddress {
56
63
Email : u .Email ,
57
- IsActivated : true ,
64
+ IsActivated : u . IsActive ,
58
65
IsPrimary : true ,
59
66
})
60
67
}
61
68
return emails , nil
62
69
}
63
70
71
+ // GetEmailAddressByID gets a user's email address by ID
72
+ func GetEmailAddressByID (uid , id int64 ) (* EmailAddress , error ) {
73
+ // User ID is required for security reasons
74
+ email := & EmailAddress {ID : id , UID : uid }
75
+ if has , err := x .Get (email ); err != nil {
76
+ return nil , err
77
+ } else if ! has {
78
+ return nil , nil
79
+ }
80
+ return email , nil
81
+ }
82
+
83
+ func isEmailActive (e Engine , email string , userID , emailID int64 ) (bool , error ) {
84
+ if len (email ) == 0 {
85
+ return true , nil
86
+ }
87
+
88
+ // Can't filter by boolean field unless it's explicit
89
+ cond := builder .NewCond ()
90
+ cond = cond .And (builder.Eq {"email" : email }, builder.Neq {"id" : emailID })
91
+ if setting .Service .RegisterEmailConfirm {
92
+ // Inactive (unvalidated) addresses don't count as active if email validation is required
93
+ cond = cond .And (builder.Eq {"is_activated" : true })
94
+ }
95
+
96
+ em := EmailAddress {}
97
+
98
+ if has , err := e .Where (cond ).Get (& em ); has || err != nil {
99
+ if has {
100
+ log .Info ("isEmailActive('%s',%d,%d) found duplicate in email ID %d" , email , userID , emailID , em .ID )
101
+ }
102
+ return has , err
103
+ }
104
+
105
+ // Can't filter by boolean field unless it's explicit
106
+ cond = builder .NewCond ()
107
+ cond = cond .And (builder.Eq {"email" : email }, builder.Neq {"id" : userID })
108
+ if setting .Service .RegisterEmailConfirm {
109
+ cond = cond .And (builder.Eq {"is_active" : true })
110
+ }
111
+
112
+ us := User {}
113
+
114
+ if has , err := e .Where (cond ).Get (& us ); has || err != nil {
115
+ if has {
116
+ log .Info ("isEmailActive('%s',%d,%d) found duplicate in user ID %d" , email , userID , emailID , us .ID )
117
+ }
118
+ return has , err
119
+ }
120
+
121
+ return false , nil
122
+ }
123
+
64
124
func isEmailUsed (e Engine , email string ) (bool , error ) {
65
125
if len (email ) == 0 {
66
126
return true , nil
@@ -118,31 +178,30 @@ func AddEmailAddresses(emails []*EmailAddress) error {
118
178
119
179
// Activate activates the email address to given user.
120
180
func (email * EmailAddress ) Activate () error {
121
- user , err := GetUserByID (email .UID )
122
- if err != nil {
181
+ sess := x .NewSession ()
182
+ defer sess .Close ()
183
+ if err := sess .Begin (); err != nil {
123
184
return err
124
185
}
125
- if user . Rands , err = GetUserSalt ( ); err != nil {
186
+ if err := email . updateActivation ( sess , true ); err != nil {
126
187
return err
127
188
}
189
+ return sess .Commit ()
190
+ }
128
191
129
- sess := x . NewSession ()
130
- defer sess . Close ( )
131
- if err = sess . Begin (); err != nil {
192
+ func ( email * EmailAddress ) updateActivation ( e Engine , activate bool ) error {
193
+ user , err := getUserByID ( e , email . UID )
194
+ if err != nil {
132
195
return err
133
196
}
134
-
135
- email .IsActivated = true
136
- if _ , err := sess .
137
- ID (email .ID ).
138
- Cols ("is_activated" ).
139
- Update (email ); err != nil {
197
+ if user .Rands , err = GetUserSalt (); err != nil {
140
198
return err
141
- } else if err = updateUserCols (sess , user , "rands" ); err != nil {
199
+ }
200
+ email .IsActivated = activate
201
+ if _ , err := e .ID (email .ID ).Cols ("is_activated" ).Update (email ); err != nil {
142
202
return err
143
203
}
144
-
145
- return sess .Commit ()
204
+ return updateUserCols (e , user , "rands" )
146
205
}
147
206
148
207
// DeleteEmailAddress deletes an email address of given user.
@@ -228,3 +287,199 @@ func MakeEmailPrimary(email *EmailAddress) error {
228
287
229
288
return sess .Commit ()
230
289
}
290
+
291
+ // SearchEmailOrderBy is used to sort the results from SearchEmails()
292
+ type SearchEmailOrderBy string
293
+
294
+ func (s SearchEmailOrderBy ) String () string {
295
+ return string (s )
296
+ }
297
+
298
+ // Strings for sorting result
299
+ const (
300
+ SearchEmailOrderByEmail SearchEmailOrderBy = "emails.email ASC, is_primary DESC, sortid ASC"
301
+ SearchEmailOrderByEmailReverse SearchEmailOrderBy = "emails.email DESC, is_primary ASC, sortid DESC"
302
+ SearchEmailOrderByName SearchEmailOrderBy = "`user`.lower_name ASC, is_primary DESC, sortid ASC"
303
+ SearchEmailOrderByNameReverse SearchEmailOrderBy = "`user`.lower_name DESC, is_primary ASC, sortid DESC"
304
+ )
305
+
306
+ // SearchEmailOptions are options to search e-mail addresses for the admin panel
307
+ type SearchEmailOptions struct {
308
+ Page int
309
+ PageSize int // Can be smaller than or equal to setting.UI.ExplorePagingNum
310
+ Keyword string
311
+ SortType SearchEmailOrderBy
312
+ IsPrimary util.OptionalBool
313
+ IsActivated util.OptionalBool
314
+ }
315
+
316
+ // SearchEmailResult is an e-mail address found in the user or email_address table
317
+ type SearchEmailResult struct {
318
+ UID int64
319
+ Email string
320
+ IsActivated bool
321
+ IsPrimary bool
322
+ // From User
323
+ Name string
324
+ FullName string
325
+ }
326
+
327
+ // SearchEmails takes options i.e. keyword and part of email name to search,
328
+ // it returns results in given range and number of total results.
329
+ func SearchEmails (opts * SearchEmailOptions ) ([]* SearchEmailResult , int64 , error ) {
330
+ // Unfortunately, UNION support for SQLite in xorm is currently broken, so we must
331
+ // build the SQL ourselves.
332
+ where := make ([]string , 0 , 5 )
333
+ args := make ([]interface {}, 0 , 5 )
334
+
335
+ emailsSQL := "(SELECT id as sortid, uid, email, is_activated, 0 as is_primary " +
336
+ "FROM email_address " +
337
+ "UNION ALL " +
338
+ "SELECT id as sortid, id AS uid, email, is_active AS is_activated, 1 as is_primary " +
339
+ "FROM `user` " +
340
+ "WHERE type = ?) AS emails"
341
+ args = append (args , UserTypeIndividual )
342
+
343
+ if len (opts .Keyword ) > 0 {
344
+ // Note: % can be injected in the Keyword parameter, but it won't do any harm.
345
+ where = append (where , "(lower(`user`.full_name) LIKE ? OR `user`.lower_name LIKE ? OR emails.email LIKE ?)" )
346
+ likeStr := "%" + strings .ToLower (opts .Keyword ) + "%"
347
+ args = append (args , likeStr )
348
+ args = append (args , likeStr )
349
+ args = append (args , likeStr )
350
+ }
351
+
352
+ switch {
353
+ case opts .IsPrimary .IsTrue ():
354
+ where = append (where , "emails.is_primary = ?" )
355
+ args = append (args , true )
356
+ case opts .IsPrimary .IsFalse ():
357
+ where = append (where , "emails.is_primary = ?" )
358
+ args = append (args , false )
359
+ }
360
+
361
+ switch {
362
+ case opts .IsActivated .IsTrue ():
363
+ where = append (where , "emails.is_activated = ?" )
364
+ args = append (args , true )
365
+ case opts .IsActivated .IsFalse ():
366
+ where = append (where , "emails.is_activated = ?" )
367
+ args = append (args , false )
368
+ }
369
+
370
+ var whereStr string
371
+ if len (where ) > 0 {
372
+ whereStr = "WHERE " + strings .Join (where , " AND " )
373
+ }
374
+
375
+ joinSQL := "FROM " + emailsSQL + " INNER JOIN `user` ON `user`.id = emails.uid " + whereStr
376
+
377
+ count , err := x .SQL ("SELECT count(*) " + joinSQL , args ... ).Count ()
378
+ if err != nil {
379
+ return nil , 0 , fmt .Errorf ("Count: %v" , err )
380
+ }
381
+
382
+ orderby := opts .SortType .String ()
383
+ if orderby == "" {
384
+ orderby = SearchEmailOrderByEmail .String ()
385
+ }
386
+
387
+ querySQL := "SELECT emails.uid, emails.email, emails.is_activated, emails.is_primary, " +
388
+ "`user`.name, `user`.full_name " + joinSQL + " ORDER BY " + orderby
389
+
390
+ if opts .PageSize == 0 || opts .PageSize > setting .UI .ExplorePagingNum {
391
+ opts .PageSize = setting .UI .ExplorePagingNum
392
+ }
393
+ if opts .Page <= 0 {
394
+ opts .Page = 1
395
+ }
396
+
397
+ rows , err := x .SQL (querySQL , args ... ).Rows (new (SearchEmailResult ))
398
+ if err != nil {
399
+ return nil , 0 , fmt .Errorf ("Emails: %v" , err )
400
+ }
401
+
402
+ // Page manually because xorm can't handle Limit() with raw SQL
403
+ defer rows .Close ()
404
+
405
+ emails := make ([]* SearchEmailResult , 0 , opts .PageSize )
406
+ skip := (opts .Page - 1 ) * opts .PageSize
407
+
408
+ for rows .Next () {
409
+ var email SearchEmailResult
410
+ if err := rows .Scan (& email ); err != nil {
411
+ return nil , 0 , err
412
+ }
413
+ if skip > 0 {
414
+ skip --
415
+ continue
416
+ }
417
+ emails = append (emails , & email )
418
+ if len (emails ) == opts .PageSize {
419
+ break
420
+ }
421
+ }
422
+
423
+ return emails , count , err
424
+ }
425
+
426
+ // ActivateUserEmail will change the activated state of an email address,
427
+ // either primary (in the user table) or secondary (in the email_address table)
428
+ func ActivateUserEmail (userID int64 , email string , primary , activate bool ) (err error ) {
429
+ sess := x .NewSession ()
430
+ defer sess .Close ()
431
+ if err = sess .Begin (); err != nil {
432
+ return err
433
+ }
434
+ if primary {
435
+ // Activate/deactivate a user's primary email address
436
+ user := User {ID : userID , Email : email }
437
+ if has , err := sess .Get (& user ); err != nil {
438
+ return err
439
+ } else if ! has {
440
+ return fmt .Errorf ("no such user: %d (%s)" , userID , email )
441
+ }
442
+ if user .IsActive == activate {
443
+ // Already in the desired state; no action
444
+ return nil
445
+ }
446
+ if activate {
447
+ if used , err := isEmailActive (sess , email , userID , 0 ); err != nil {
448
+ return fmt .Errorf ("isEmailActive(): %v" , err )
449
+ } else if used {
450
+ return ErrEmailAlreadyUsed {Email : email }
451
+ }
452
+ }
453
+ user .IsActive = activate
454
+ if user .Rands , err = GetUserSalt (); err != nil {
455
+ return fmt .Errorf ("generate salt: %v" , err )
456
+ }
457
+ if err = updateUserCols (sess , & user , "is_active" , "rands" ); err != nil {
458
+ return fmt .Errorf ("updateUserCols(): %v" , err )
459
+ }
460
+ } else {
461
+ // Activate/deactivate a user's secondary email address
462
+ // First check if there's another user active with the same address
463
+ addr := EmailAddress {UID : userID , Email : email }
464
+ if has , err := sess .Get (& addr ); err != nil {
465
+ return err
466
+ } else if ! has {
467
+ return fmt .Errorf ("no such email: %d (%s)" , userID , email )
468
+ }
469
+ if addr .IsActivated == activate {
470
+ // Already in the desired state; no action
471
+ return nil
472
+ }
473
+ if activate {
474
+ if used , err := isEmailActive (sess , email , 0 , addr .ID ); err != nil {
475
+ return fmt .Errorf ("isEmailActive(): %v" , err )
476
+ } else if used {
477
+ return ErrEmailAlreadyUsed {Email : email }
478
+ }
479
+ }
480
+ if err = addr .updateActivation (sess , activate ); err != nil {
481
+ return fmt .Errorf ("updateActivation(): %v" , err )
482
+ }
483
+ }
484
+ return sess .Commit ()
485
+ }
0 commit comments