Compare commits

...

26 Commits

Author SHA1 Message Date
Lauris BH
832e2ebe91 Changelog for release 1.4.0 (#3714) 2018-03-25 09:12:48 +08:00
Jonas Franz
68134e6441 Escape branch name in dropdown menu (#3692)
Signed-off-by: Jonas Franz <info@jonasfranz.software>

(cherry picked from commit 61ce616)
Signed-off-by: Jonas Franz <info@jonasfranz.software>
2018-03-19 22:31:01 +08:00
Lauris BH
3022681432 Changelog for 1.4.0-rc3 (#3679) 2018-03-16 22:11:34 +02:00
Lauris BH
c0e0fb7d39 Refactor and simplify redirect to url (#3674) (#3676) 2018-03-16 19:59:47 +08:00
Lauris BH
0c612124f9 Update markbates/goth libary to fix OAuth2 support (#3661) (#3663) 2018-03-13 09:08:26 +02:00
Lauris BH
5d0c9872a9 Fix MySQL and PostgreSQL column drop SQL (#3649) (#3651) 2018-03-10 10:27:19 +08:00
Lauris BH
92a3061753 Fix column removal in MSSQL (#3638) (#3640)
* Fix column removal in MSSQL

* Use xorm session in MSSQL drop column operations

* Add transaction as MSSQL alter table is transactional
2018-03-07 11:59:20 +02:00
Lauris BH
efc5a7171b Fix wiki inter-links with spaces (#3560) (#3632) 2018-03-06 16:43:50 +08:00
Lauris BH
93f34fd8a2 Changelog for release 1.4.0-rc2 (#3610) 2018-03-03 00:10:37 +02:00
Wendell Sun
dd784396ce Fix query protected branch bug (#3563) (#3571)
Signed-off-by: Wendell Sun <iwendellsun@gmail.com>
2018-02-24 00:07:25 +02:00
Wendell Sun
4a0ce6896b Fix remove team member issue (#3566) (#3570)
Signed-off-by: Wendell Sun <iwendellsun@gmail.com>
2018-02-23 20:02:45 +02:00
Wendell Sun
d87fb0a6fd Fix the protected branch panic issue (#3567) (#3569)
Signed-off-by: Wendell Sun <iwendellsun@gmail.com>
2018-02-23 19:27:34 +02:00
Bo-Yi Wu
e55eaa8545 if Mirrors repo no content is fetched, updated time should not be changed #3551 (#3565)
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2018-02-23 13:48:40 +02:00
Wendell Sun
423c642fdb Bug fix for repo releases sorted (#3522) (#3555)
Signed-off-by: Wendell Sun <iwendellsun@gmail.com>

Use TimeStampNow function
2018-02-22 09:56:38 +08:00
Bo-Yi Wu
ed2ba84525 refactor: reduce sql query in retrieveFeeds (#3554) 2018-02-21 18:15:00 +02:00
Lauris BH
e8015a59bb Add issue closed time column to fix activity closed issues list (#3537) (#3540)
Signed-off-by: Lauris Bukšis-Haberkorns <lauris@nix.lv>
2018-02-19 21:51:37 +02:00
Lauris BH
8327300809 Update markbates/goth library (#3533) (#3539)
Signed-off-by: Lauris Bukšis-Haberkorns <lauris@nix.lv>
2018-02-19 20:10:38 +08:00
Lauris BH
ade183957d Force remove test repo root path in case previous test is still locking it (#3528) (#3536)
Signed-off-by: Lauris Bukšis-Haberkorns <lauris@nix.lv>
2018-02-19 08:29:37 +02:00
Lauris BH
c503eac206 Fix escaping changed title in comments (#3530) (#3534)
* Fix escaping changed title in comments

* Fix escaping of wiki page titile

Signed-off-by: Lauris Bukšis-Haberkorns <lauris@nix.lv>
2018-02-19 10:33:51 +08:00
Jonas Bröms
221e502297 Clarify Indexer MAX_FILE_SIZE (#3469) (#3474) 2018-02-15 11:20:41 +02:00
Codruț Constantin Gușoi
1c3d712fe7 Fixes missing avatars in offline mode (#3471) (#3477)
Signed-off-by: Codruț Constantin Gușoi <codrut.gusoi@gmail.com>
2018-02-14 23:37:35 +02:00
Ethan Koenig
85f90187f3 Improve wiki test (#3493) (#3511) 2018-02-14 22:40:59 +02:00
Jonas Franz
c0675ef6c2 Escape search query (Backport 1.4) (#3488)
* Escape search query

Signed-off-by: Jonas Franz <info@jonasfranz.de>

(cherry picked from commit 2970889)

* Reordered imports

Signed-off-by: Jonas Franz <info@jonasfranz.de>
2018-02-11 21:25:02 +02:00
Ethan Koenig
4e27cc4813 Fix synchronization bug in repo indexer (#3455) (#3461) 2018-02-08 08:42:45 +02:00
Lauris BH
f61ef28f26 Fix rendering of wiki page list if wiki repo contains other files (#3454) (#3463)
* Fix rendering of wiki page list if wiki repo contains other files

* Improve wiki filename tests
2018-02-06 07:59:44 +02:00
Lauris Bukšis-Haberkorns
b27e10d6e4 Change 1.4.0-rc1 version release date
Signed-off-by: Lauris Bukšis-Haberkorns <lauris@nix.lv>
2018-02-01 10:22:24 +02:00
50 changed files with 899 additions and 377 deletions

View File

@@ -4,11 +4,15 @@ 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.io). been added to each release, please refer to the [blog](https://blog.gitea.io).
## [1.4.0-rc1](https://github.com/go-gitea/gitea/releases/tag/v1.4.0-rc1) - 2018-01-31 ## [1.4.0](https://github.com/go-gitea/gitea/releases/tag/v1.4.0) - 2018-03-25
* BREAKING * BREAKING
* Drop deprecated GOGS\_WORK\_DIR use (#2946) * Drop deprecated GOGS\_WORK\_DIR use (#2946)
* Fix API status code for hook creation (#2814) * Fix API status code for hook creation (#2814)
* SECURITY * SECURITY
* Escape branch name in dropdown menu (#3691) (#3692)
* Refactor and simplify to correctly validate redirect to URL (#3674) (#3676)
* Fix escaping changed title in comments (#3530) (#3534)
 * Escape search query (#3486) (#3488)
* Sanitize logs for mirror sync (#3057) * Sanitize logs for mirror sync (#3057)
* FEATURE * FEATURE
* Serve .patch and .diff for pull requests (#3305, #3293) * Serve .patch and .diff for pull requests (#3305, #3293)
@@ -24,6 +28,17 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
* Add dingtalk webhook (#2777) * Add dingtalk webhook (#2777)
* Responsive view (#2750) * Responsive view (#2750)
* BUGFIXES * BUGFIXES
* Fix wiki inter-links with spaces (#3560) (#3632)
* Fix query protected branch bug (#3563) (#3571)
* Fix remove team member issue (#3566) (#3570)
* Fix the protected branch panic issue (#3567) (#3569)
* If Mirrors repository no content is fetched, updated time should not be changed (#3551) (#3565)
* Bug fix for mirrored repository releases sorted (#3522) (#3555)
* Add issue closed time column to fix activity closed issues list (#3537) (#3540)
 * Update markbates/goth library to support OAuth2 with new dropbox API (#3533) (#3539)
 * Fixes missing avatars in offline mode (#3471) (#3477)
 * Fix synchronization bug in repo indexer (#3455) (#3461)
 * Fix rendering of wiki page list if wiki repo contains other files (#3454) (#3463)
* Fix webhook X-GitHub-* headers casing for better compatibility (#3429) * Fix webhook X-GitHub-* headers casing for better compatibility (#3429)
* Add content type and doctype to requests made with go-get (#3426, #3423) * Add content type and doctype to requests made with go-get (#3426, #3423)
* Fix SQL type error for webhooks (#3424) * Fix SQL type error for webhooks (#3424)

View File

@@ -128,7 +128,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `REPO_INDEXER_ENABLED`: **false**: Enables code search (uses a lot of disk space). - `REPO_INDEXER_ENABLED`: **false**: Enables code search (uses a lot of disk space).
- `REPO_INDEXER_PATH`: **indexers/repos.bleve**: Index file used for code search. - `REPO_INDEXER_PATH`: **indexers/repos.bleve**: Index file used for code search.
- `UPDATE_BUFFER_LEN`: **20**: Buffer length of index request. - `UPDATE_BUFFER_LEN`: **20**: Buffer length of index request.
- `MAX_FILE_SIZE`: **1048576**: Maximum size in bytes of each index files. - `MAX_FILE_SIZE`: **1048576**: Maximum size in bytes of files to be indexed.
## Security (`security`) ## Security (`security`)

View File

@@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func testPullCreate(t *testing.T, session *TestSession, user, repo, branch string) *httptest.ResponseRecorder { func testPullCreate(t *testing.T, session *TestSession, user, repo, branch, title string) *httptest.ResponseRecorder {
req := NewRequest(t, "GET", path.Join(user, repo)) req := NewRequest(t, "GET", path.Join(user, repo))
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
@@ -35,7 +35,7 @@ func testPullCreate(t *testing.T, session *TestSession, user, repo, branch strin
assert.True(t, exists, "The template has changed") assert.True(t, exists, "The template has changed")
req = NewRequestWithValues(t, "POST", link, map[string]string{ req = NewRequestWithValues(t, "POST", link, map[string]string{
"_csrf": htmlDoc.GetCSRF(), "_csrf": htmlDoc.GetCSRF(),
"title": "This is a pull title", "title": title,
}) })
resp = session.MakeRequest(t, req, http.StatusFound) resp = session.MakeRequest(t, req, http.StatusFound)
@@ -47,7 +47,7 @@ func TestPullCreate(t *testing.T) {
session := loginUser(t, "user1") session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
resp := testPullCreate(t, session, "user1", "repo1", "master") resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
// check the redirected URL // check the redirected URL
url := resp.HeaderMap.Get("Location") url := resp.HeaderMap.Get("Location")
@@ -68,3 +68,38 @@ func TestPullCreate(t *testing.T) {
assert.Regexp(t, `Subject: \[PATCH\] Update 'README.md'`, resp.Body) assert.Regexp(t, `Subject: \[PATCH\] Update 'README.md'`, resp.Body)
assert.NotRegexp(t, "diff.*diff", resp.Body) // not two diffs, just one assert.NotRegexp(t, "diff.*diff", resp.Body) // not two diffs, just one
} }
func TestPullCreate_TitleEscape(t *testing.T) {
prepareTestEnv(t)
session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
resp := testPullCreate(t, session, "user1", "repo1", "master", "<i>XSS PR</i>")
// check the redirected URL
url := resp.HeaderMap.Get("Location")
assert.Regexp(t, "^/user2/repo1/pulls/[0-9]*$", url)
// Edit title
req := NewRequest(t, "GET", url)
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
editTestTitleURL, exists := htmlDoc.doc.Find("#save-edit-title").First().Attr("data-update-url")
assert.True(t, exists, "The template has changed")
req = NewRequestWithValues(t, "POST", editTestTitleURL, map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"title": "<u>XSS PR</u>",
})
session.MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", url)
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
titleHTML, err := htmlDoc.doc.Find(".comments .event .text b").First().Html()
assert.NoError(t, err)
assert.Equal(t, "&lt;i&gt;XSS PR&lt;/i&gt;", titleHTML)
titleHTML, err = htmlDoc.doc.Find(".comments .event .text b").Next().Html()
assert.NoError(t, err)
assert.Equal(t, "&lt;u&gt;XSS PR&lt;/u&gt;", titleHTML)
}

View File

@@ -56,7 +56,7 @@ func TestPullMerge(t *testing.T) {
testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
resp := testPullCreate(t, session, "user1", "repo1", "master") resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
elem := strings.Split(test.RedirectURL(resp), "/") elem := strings.Split(test.RedirectURL(resp), "/")
assert.EqualValues(t, "pulls", elem[3]) assert.EqualValues(t, "pulls", elem[3])
@@ -69,7 +69,7 @@ func TestPullRebase(t *testing.T) {
testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
resp := testPullCreate(t, session, "user1", "repo1", "master") resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
elem := strings.Split(test.RedirectURL(resp), "/") elem := strings.Split(test.RedirectURL(resp), "/")
assert.EqualValues(t, "pulls", elem[3]) assert.EqualValues(t, "pulls", elem[3])
@@ -83,7 +83,7 @@ func TestPullSquash(t *testing.T) {
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n")
resp := testPullCreate(t, session, "user1", "repo1", "master") resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
elem := strings.Split(test.RedirectURL(resp), "/") elem := strings.Split(test.RedirectURL(resp), "/")
assert.EqualValues(t, "pulls", elem[3]) assert.EqualValues(t, "pulls", elem[3])
@@ -96,7 +96,7 @@ func TestPullCleanUpAfterMerge(t *testing.T) {
testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n")
resp := testPullCreate(t, session, "user1", "repo1", "feature/test") resp := testPullCreate(t, session, "user1", "repo1", "feature/test", "This is a pull title")
elem := strings.Split(test.RedirectURL(resp), "/") elem := strings.Split(test.RedirectURL(resp), "/")
assert.EqualValues(t, "pulls", elem[3]) assert.EqualValues(t, "pulls", elem[3])

View File

@@ -22,16 +22,16 @@ func TestRepoActivity(t *testing.T) {
// Create PRs (1 merged & 2 proposed) // Create PRs (1 merged & 2 proposed)
testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
resp := testPullCreate(t, session, "user1", "repo1", "master") resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
elem := strings.Split(test.RedirectURL(resp), "/") elem := strings.Split(test.RedirectURL(resp), "/")
assert.EqualValues(t, "pulls", elem[3]) assert.EqualValues(t, "pulls", elem[3])
testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleMerge) testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleMerge)
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feat/better_readme", "README.md", "Hello, World (Edited Again)\n") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feat/better_readme", "README.md", "Hello, World (Edited Again)\n")
testPullCreate(t, session, "user1", "repo1", "feat/better_readme") testPullCreate(t, session, "user1", "repo1", "feat/better_readme", "This is a pull title")
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feat/much_better_readme", "README.md", "Hello, World (Edited More)\n") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feat/much_better_readme", "README.md", "Hello, World (Edited More)\n")
testPullCreate(t, session, "user1", "repo1", "feat/much_better_readme") testPullCreate(t, session, "user1", "repo1", "feat/much_better_readme", "This is a pull title")
// Create issues (3 new issues) // Create issues (3 new issues)
testNewIssue(t, session, "user2", "repo1", "Issue 1", "Description 1") testNewIssue(t, session, "user2", "repo1", "Issue 1", "Description 1")

View File

@@ -742,5 +742,14 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
} }
actions := make([]*Action, 0, 20) actions := make([]*Action, 0, 20)
return actions, x.Limit(20).Desc("id").Where(cond).Find(&actions)
if err := x.Limit(20).Desc("id").Where(cond).Find(&actions); err != nil {
return nil, fmt.Errorf("Find: %v", err)
}
if err := ActionList(actions).LoadAttributes(); err != nil {
return nil, fmt.Errorf("LoadAttributes: %v", err)
}
return actions, nil
} }

98
models/action_list.go Normal file
View File

@@ -0,0 +1,98 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import "fmt"
// ActionList defines a list of actions
type ActionList []*Action
func (actions ActionList) getUserIDs() []int64 {
userIDs := make(map[int64]struct{}, len(actions))
for _, action := range actions {
if _, ok := userIDs[action.ActUserID]; !ok {
userIDs[action.ActUserID] = struct{}{}
}
}
return keysInt64(userIDs)
}
func (actions ActionList) loadUsers(e Engine) ([]*User, error) {
if len(actions) == 0 {
return nil, nil
}
userIDs := actions.getUserIDs()
userMaps := make(map[int64]*User, len(userIDs))
err := e.
In("id", userIDs).
Find(&userMaps)
if err != nil {
return nil, fmt.Errorf("find user: %v", err)
}
for _, action := range actions {
action.ActUser = userMaps[action.ActUserID]
}
return valuesUser(userMaps), nil
}
// LoadUsers loads actions' all users
func (actions ActionList) LoadUsers() ([]*User, error) {
return actions.loadUsers(x)
}
func (actions ActionList) getRepoIDs() []int64 {
repoIDs := make(map[int64]struct{}, len(actions))
for _, action := range actions {
if _, ok := repoIDs[action.RepoID]; !ok {
repoIDs[action.RepoID] = struct{}{}
}
}
return keysInt64(repoIDs)
}
func (actions ActionList) loadRepositories(e Engine) ([]*Repository, error) {
if len(actions) == 0 {
return nil, nil
}
repoIDs := actions.getRepoIDs()
repoMaps := make(map[int64]*Repository, len(repoIDs))
err := e.
In("id", repoIDs).
Find(&repoMaps)
if err != nil {
return nil, fmt.Errorf("find repository: %v", err)
}
for _, action := range actions {
action.Repo = repoMaps[action.RepoID]
}
return valuesRepository(repoMaps), nil
}
// LoadRepositories loads actions' all repositories
func (actions ActionList) LoadRepositories() ([]*Repository, error) {
return actions.loadRepositories(x)
}
// loadAttributes loads all attributes
func (actions ActionList) loadAttributes(e Engine) (err error) {
if _, err = actions.loadUsers(e); err != nil {
return
}
if _, err = actions.loadRepositories(e); err != nil {
return
}
return nil
}
// LoadAttributes loads attributes of the actions
func (actions ActionList) LoadAttributes() error {
return actions.loadAttributes(x)
}

View File

@@ -6,7 +6,6 @@ package models
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
@@ -70,7 +69,7 @@ func GetProtectedBranchByRepoID(RepoID int64) ([]*ProtectedBranch, error) {
// GetProtectedBranchBy getting protected branch by ID/Name // GetProtectedBranchBy getting protected branch by ID/Name
func GetProtectedBranchBy(repoID int64, BranchName string) (*ProtectedBranch, error) { func GetProtectedBranchBy(repoID int64, BranchName string) (*ProtectedBranch, error) {
rel := &ProtectedBranch{RepoID: repoID, BranchName: strings.ToLower(BranchName)} rel := &ProtectedBranch{RepoID: repoID, BranchName: BranchName}
has, err := x.Get(rel) has, err := x.Get(rel)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -156,6 +155,10 @@ func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) {
// IsProtectedBranch checks if branch is protected // IsProtectedBranch checks if branch is protected
func (repo *Repository) IsProtectedBranch(branchName string, doer *User) (bool, error) { func (repo *Repository) IsProtectedBranch(branchName string, doer *User) (bool, error) {
if doer == nil {
return true, nil
}
protectedBranch := &ProtectedBranch{ protectedBranch := &ProtectedBranch{
RepoID: repo.ID, RepoID: repo.ID,
BranchName: branchName, BranchName: branchName,

View File

@@ -216,6 +216,21 @@ func (err ErrWikiReservedName) Error() string {
return fmt.Sprintf("wiki title is reserved: %s", err.Title) return fmt.Sprintf("wiki title is reserved: %s", err.Title)
} }
// ErrWikiInvalidFileName represents an invalid wiki file name.
type ErrWikiInvalidFileName struct {
FileName string
}
// IsErrWikiInvalidFileName checks if an error is an ErrWikiInvalidFileName.
func IsErrWikiInvalidFileName(err error) bool {
_, ok := err.(ErrWikiInvalidFileName)
return ok
}
func (err ErrWikiInvalidFileName) Error() string {
return fmt.Sprintf("Invalid wiki filename: %s", err.FileName)
}
// __________ ___. .__ .__ ____ __. // __________ ___. .__ .__ ____ __.
// \______ \__ _\_ |__ | | |__| ____ | |/ _|____ ___.__. // \______ \__ _\_ |__ | | |__| ____ | |/ _|____ ___.__.
// | ___/ | \ __ \| | | |/ ___\ | <_/ __ < | | // | ___/ | \ __ \| | | |/ ___\ | <_/ __ < | |

View File

@@ -49,6 +49,7 @@ type Issue struct {
DeadlineUnix util.TimeStamp `xorm:"INDEX"` DeadlineUnix util.TimeStamp `xorm:"INDEX"`
CreatedUnix util.TimeStamp `xorm:"INDEX created"` CreatedUnix util.TimeStamp `xorm:"INDEX created"`
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
ClosedUnix util.TimeStamp `xorm:"INDEX"`
Attachments []*Attachment `xorm:"-"` Attachments []*Attachment `xorm:"-"`
Comments []*Comment `xorm:"-"` Comments []*Comment `xorm:"-"`
@@ -612,8 +613,13 @@ func (issue *Issue) changeStatus(e *xorm.Session, doer *User, repo *Repository,
return nil return nil
} }
issue.IsClosed = isClosed issue.IsClosed = isClosed
if isClosed {
issue.ClosedUnix = util.TimeStampNow()
} else {
issue.ClosedUnix = 0
}
if err = updateIssueCols(e, issue, "is_closed"); err != nil { if err = updateIssueCols(e, issue, "is_closed", "closed_unix"); err != nil {
return err return err
} }

View File

@@ -214,13 +214,15 @@ func TestChangeMilestoneIssueStats(t *testing.T) {
"is_closed=0").(*Issue) "is_closed=0").(*Issue)
issue.IsClosed = true issue.IsClosed = true
_, err := x.Cols("is_closed").Update(issue) issue.ClosedUnix = util.TimeStampNow()
_, err := x.Cols("is_closed", "closed_unix").Update(issue)
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, changeMilestoneIssueStats(x.NewSession(), issue)) assert.NoError(t, changeMilestoneIssueStats(x.NewSession(), issue))
CheckConsistencyFor(t, &Milestone{}) CheckConsistencyFor(t, &Milestone{})
issue.IsClosed = false issue.IsClosed = false
_, err = x.Cols("is_closed").Update(issue) issue.ClosedUnix = 0
_, err = x.Cols("is_closed", "closed_unix").Update(issue)
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, changeMilestoneIssueStats(x.NewSession(), issue)) assert.NoError(t, changeMilestoneIssueStats(x.NewSession(), issue))
CheckConsistencyFor(t, &Milestone{}) CheckConsistencyFor(t, &Milestone{})

View File

@@ -166,6 +166,8 @@ var migrations = []Migration{
NewMigration("add writable deploy keys", addModeToDeploKeys), NewMigration("add writable deploy keys", addModeToDeploKeys),
// v56 -> v57 // v56 -> v57
NewMigration("remove is_owner, num_teams columns from org_user", removeIsOwnerColumnFromOrgUser), NewMigration("remove is_owner, num_teams columns from org_user", removeIsOwnerColumnFromOrgUser),
// v57 -> v58
NewMigration("add closed_unix column for issues", addIssueClosedTime),
} }
// Migrate database to current version // Migrate database to current version
@@ -215,6 +217,66 @@ Please try to upgrade to a lower version (>= v0.6.0) first, then upgrade to curr
return nil return nil
} }
func dropTableColumns(x *xorm.Engine, tableName string, columnNames ...string) (err error) {
if tableName == "" || len(columnNames) == 0 {
return nil
}
switch {
case setting.UseSQLite3:
log.Warn("Unable to drop columns in SQLite")
case setting.UseMySQL, setting.UseTiDB, setting.UsePostgreSQL:
cols := ""
for _, col := range columnNames {
if cols != "" {
cols += ", "
}
cols += "DROP COLUMN `" + col + "`"
}
if _, err := x.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil {
return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err)
}
case setting.UseMSSQL:
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
cols := ""
for _, col := range columnNames {
if cols != "" {
cols += ", "
}
cols += "`" + strings.ToLower(col) + "`"
}
sql := fmt.Sprintf("SELECT Name FROM SYS.DEFAULT_CONSTRAINTS WHERE PARENT_OBJECT_ID = OBJECT_ID('%[1]s') AND PARENT_COLUMN_ID IN (SELECT column_id FROM sys.columns WHERE lower(NAME) IN (%[2]s) AND object_id = OBJECT_ID('%[1]s'))",
tableName, strings.Replace(cols, "`", "'", -1))
constraints := make([]string, 0)
if err := sess.SQL(sql).Find(&constraints); err != nil {
sess.Rollback()
return fmt.Errorf("Find constraints: %v", err)
}
for _, constraint := range constraints {
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` DROP CONSTRAINT `%s`", tableName, constraint)); err != nil {
sess.Rollback()
return fmt.Errorf("Drop table `%s` constraint `%s`: %v", tableName, constraint, err)
}
}
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` DROP COLUMN %s", tableName, cols)); err != nil {
sess.Rollback()
return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err)
}
return sess.Commit()
default:
log.Fatal(4, "Unrecognized DB")
}
return nil
}
func fixLocaleFileLoadPanic(_ *xorm.Engine) error { func fixLocaleFileLoadPanic(_ *xorm.Engine) error {
cfg, err := ini.Load(setting.CustomConf) cfg, err := ini.Load(setting.CustomConf)
if err != nil { if err != nil {

View File

@@ -5,29 +5,9 @@
package migrations package migrations
import ( import (
"fmt"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/go-xorm/xorm" "github.com/go-xorm/xorm"
) )
func removeIsOwnerColumnFromOrgUser(x *xorm.Engine) (err error) { func removeIsOwnerColumnFromOrgUser(x *xorm.Engine) (err error) {
switch { return dropTableColumns(x, "org_user", "is_owner", "num_teams")
case setting.UseSQLite3:
log.Warn("Unable to drop columns in SQLite")
case setting.UseMySQL, setting.UseTiDB, setting.UsePostgreSQL:
if _, err := x.Exec("ALTER TABLE org_user DROP COLUMN is_owner, DROP COLUMN num_teams"); err != nil {
return fmt.Errorf("DROP COLUMN org_user.is_owner, org_user.num_teams: %v", err)
}
case setting.UseMSSQL:
if _, err := x.Exec("ALTER TABLE org_user DROP COLUMN is_owner, num_teams"); err != nil {
return fmt.Errorf("DROP COLUMN org_user.is_owner, org_user.num_teams: %v", err)
}
default:
log.Fatal(4, "Unrecognized DB")
}
return nil
} }

30
models/migrations/v57.go Normal file
View File

@@ -0,0 +1,30 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package migrations
import (
"fmt"
"code.gitea.io/gitea/modules/util"
"github.com/go-xorm/xorm"
)
func addIssueClosedTime(x *xorm.Engine) error {
// Issue see models/issue.go
type Issue struct {
ClosedUnix util.TimeStamp `xorm:"INDEX"`
}
if err := x.Sync2(new(Issue)); err != nil {
return fmt.Errorf("Sync2: %v", err)
}
if _, err := x.Exec("UPDATE `issue` SET `closed_unix` = `updated_unix` WHERE `is_closed` = ?", true); err != nil {
return err
}
return nil
}

View File

@@ -436,8 +436,7 @@ func AddOrgUser(orgID, uid int64) error {
return sess.Commit() return sess.Commit()
} }
// RemoveOrgUser removes user from given organization. func removeOrgUser(sess *xorm.Session, orgID, userID int64) error {
func RemoveOrgUser(orgID, userID int64) error {
ou := new(OrgUser) ou := new(OrgUser)
has, err := x. has, err := x.
@@ -473,12 +472,6 @@ func RemoveOrgUser(orgID, userID int64) error {
} }
} }
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if _, err := sess.ID(ou.ID).Delete(ou); err != nil { if _, err := sess.ID(ou.ID).Delete(ou); err != nil {
return err return err
} else if _, err = sess.Exec("UPDATE `user` SET num_members=num_members-1 WHERE id=?", orgID); err != nil { } else if _, err = sess.Exec("UPDATE `user` SET num_members=num_members-1 WHERE id=?", orgID); err != nil {
@@ -520,6 +513,19 @@ func RemoveOrgUser(orgID, userID int64) error {
} }
} }
return nil
}
// RemoveOrgUser removes user from given organization.
func RemoveOrgUser(orgID, userID int64) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if err := removeOrgUser(sess, orgID, userID); err != nil {
return err
}
return sess.Commit() return sess.Commit()
} }

View File

@@ -10,6 +10,8 @@ import (
"strings" "strings"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"github.com/go-xorm/xorm"
) )
const ownerTeamName = "Owners" const ownerTeamName = "Owners"
@@ -521,7 +523,7 @@ func AddTeamMember(team *Team, userID int64) error {
return sess.Commit() return sess.Commit()
} }
func removeTeamMember(e Engine, team *Team, userID int64) error { func removeTeamMember(e *xorm.Session, team *Team, userID int64) error {
isMember, err := isTeamMember(e, team.OrgID, team.ID, userID) isMember, err := isTeamMember(e, team.OrgID, team.ID, userID)
if err != nil || !isMember { if err != nil || !isMember {
return err return err
@@ -558,6 +560,16 @@ func removeTeamMember(e Engine, team *Team, userID int64) error {
} }
} }
// Check if the user is a member of any team in the organization.
if count, err := e.Count(&TeamUser{
UID: userID,
OrgID: team.OrgID,
}); err != nil {
return err
} else if count == 0 {
return removeOrgUser(e, team.OrgID, userID)
}
return nil return nil
} }

View File

@@ -36,7 +36,7 @@ type Release struct {
IsPrerelease bool `xorm:"NOT NULL DEFAULT false"` IsPrerelease bool `xorm:"NOT NULL DEFAULT false"`
IsTag bool `xorm:"NOT NULL DEFAULT false"` IsTag bool `xorm:"NOT NULL DEFAULT false"`
Attachments []*Attachment `xorm:"-"` Attachments []*Attachment `xorm:"-"`
CreatedUnix util.TimeStamp `xorm:"created INDEX"` CreatedUnix util.TimeStamp `xorm:"INDEX"`
} }
func (r *Release) loadAttributes(e Engine) error { func (r *Release) loadAttributes(e Engine) error {
@@ -134,6 +134,8 @@ func createTag(gitRepo *git.Repository, rel *Release) error {
if err != nil { if err != nil {
return fmt.Errorf("CommitsCount: %v", err) return fmt.Errorf("CommitsCount: %v", err)
} }
} else {
rel.CreatedUnix = util.TimeStampNow()
} }
return nil return nil
} }

View File

@@ -176,7 +176,7 @@ func (stats *ActivityStats) FillIssues(repoID int64, fromTime time.Time) error {
// Closed issues // Closed issues
sess := issuesForActivityStatement(repoID, fromTime, true, false) sess := issuesForActivityStatement(repoID, fromTime, true, false)
sess.OrderBy("issue.updated_unix DESC") sess.OrderBy("issue.closed_unix DESC")
stats.ClosedIssues = make(IssueList, 0) stats.ClosedIssues = make(IssueList, 0)
if err = sess.Find(&stats.ClosedIssues); err != nil { if err = sess.Find(&stats.ClosedIssues); err != nil {
return err return err
@@ -228,7 +228,11 @@ func issuesForActivityStatement(repoID int64, fromTime time.Time, closed, unreso
if !unresolved { if !unresolved {
sess.And("issue.is_pull = ?", false) sess.And("issue.is_pull = ?", false)
sess.And("issue.created_unix >= ?", fromTime.Unix()) if closed {
sess.And("issue.closed_unix >= ?", fromTime.Unix())
} else {
sess.And("issue.created_unix >= ?", fromTime.Unix())
}
} else { } else {
sess.And("issue.created_unix < ?", fromTime.Unix()) sess.And("issue.created_unix < ?", fromTime.Unix())
sess.And("issue.updated_unix >= ?", fromTime.Unix()) sess.And("issue.updated_unix >= ?", fromTime.Unix())

View File

@@ -5,9 +5,7 @@
package models package models
import ( import (
"io/ioutil" "fmt"
"os"
"path"
"strconv" "strconv"
"strings" "strings"
@@ -16,8 +14,6 @@ import (
"code.gitea.io/gitea/modules/indexer" "code.gitea.io/gitea/modules/indexer"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/Unknwon/com"
) )
// RepoIndexerStatus status of a repo's entry in the repo indexer // RepoIndexerStatus status of a repo's entry in the repo indexer
@@ -132,7 +128,11 @@ func populateRepoIndexer(maxRepoID int64) {
} }
func updateRepoIndexer(repo *Repository) error { func updateRepoIndexer(repo *Repository) error {
changes, err := getRepoChanges(repo) sha, err := getDefaultBranchSha(repo)
if err != nil {
return err
}
changes, err := getRepoChanges(repo, sha)
if err != nil { if err != nil {
return err return err
} else if changes == nil { } else if changes == nil {
@@ -140,12 +140,12 @@ func updateRepoIndexer(repo *Repository) error {
} }
batch := indexer.RepoIndexerBatch() batch := indexer.RepoIndexerBatch()
for _, filename := range changes.UpdatedFiles { for _, update := range changes.Updates {
if err := addUpdate(filename, repo, batch); err != nil { if err := addUpdate(update, repo, batch); err != nil {
return err return err
} }
} }
for _, filename := range changes.RemovedFiles { for _, filename := range changes.RemovedFilenames {
if err := addDelete(filename, repo, batch); err != nil { if err := addDelete(filename, repo, batch); err != nil {
return err return err
} }
@@ -153,56 +153,61 @@ func updateRepoIndexer(repo *Repository) error {
if err = batch.Flush(); err != nil { if err = batch.Flush(); err != nil {
return err return err
} }
return updateLastIndexSync(repo) return repo.updateIndexerStatus(sha)
} }
// repoChanges changes (file additions/updates/removals) to a repo // repoChanges changes (file additions/updates/removals) to a repo
type repoChanges struct { type repoChanges struct {
UpdatedFiles []string Updates []fileUpdate
RemovedFiles []string RemovedFilenames []string
}
type fileUpdate struct {
Filename string
BlobSha string
}
func getDefaultBranchSha(repo *Repository) (string, error) {
stdout, err := git.NewCommand("show-ref", "-s", repo.DefaultBranch).RunInDir(repo.RepoPath())
if err != nil {
return "", err
}
return strings.TrimSpace(stdout), nil
} }
// getRepoChanges returns changes to repo since last indexer update // getRepoChanges returns changes to repo since last indexer update
func getRepoChanges(repo *Repository) (*repoChanges, error) { func getRepoChanges(repo *Repository, revision string) (*repoChanges, error) {
repoWorkingPool.CheckIn(com.ToStr(repo.ID)) if err := repo.getIndexerStatus(); err != nil {
defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
if err := repo.UpdateLocalCopyBranch(""); err != nil {
return nil, err
} else if !git.IsBranchExist(repo.LocalCopyPath(), repo.DefaultBranch) {
// repo does not have any commits yet, so nothing to update
return nil, nil
} else if err = repo.UpdateLocalCopyBranch(repo.DefaultBranch); err != nil {
return nil, err
} else if err = repo.getIndexerStatus(); err != nil {
return nil, err return nil, err
} }
if len(repo.IndexerStatus.CommitSha) == 0 { if len(repo.IndexerStatus.CommitSha) == 0 {
return genesisChanges(repo) return genesisChanges(repo, revision)
} }
return nonGenesisChanges(repo) return nonGenesisChanges(repo, revision)
} }
func addUpdate(filename string, repo *Repository, batch *indexer.Batch) error { func addUpdate(update fileUpdate, repo *Repository, batch *indexer.Batch) error {
filepath := path.Join(repo.LocalCopyPath(), filename) stdout, err := git.NewCommand("cat-file", "-s", update.BlobSha).
if stat, err := os.Stat(filepath); err != nil { RunInDir(repo.RepoPath())
if err != nil {
return err return err
} else if stat.Size() > setting.Indexer.MaxIndexerFileSize { }
return nil if size, err := strconv.Atoi(strings.TrimSpace(stdout)); err != nil {
} else if stat.IsDir() { return fmt.Errorf("Misformatted git cat-file output: %v", err)
// file could actually be a directory, if it is the root of a submodule. } else if int64(size) > setting.Indexer.MaxIndexerFileSize {
// We do not index submodule contents, so don't do anything.
return nil return nil
} }
fileContents, err := ioutil.ReadFile(filepath)
fileContents, err := git.NewCommand("cat-file", "blob", update.BlobSha).
RunInDirBytes(repo.RepoPath())
if err != nil { if err != nil {
return err return err
} else if !base.IsTextFile(fileContents) { } else if !base.IsTextFile(fileContents) {
return nil return nil
} }
return batch.Add(indexer.RepoIndexerUpdate{ return batch.Add(indexer.RepoIndexerUpdate{
Filepath: filename, Filepath: update.Filename,
Op: indexer.RepoIndexerOpUpdate, Op: indexer.RepoIndexerOpUpdate,
Data: &indexer.RepoIndexerData{ Data: &indexer.RepoIndexerData{
RepoID: repo.ID, RepoID: repo.ID,
@@ -221,42 +226,76 @@ func addDelete(filename string, repo *Repository, batch *indexer.Batch) error {
}) })
} }
// genesisChanges get changes to add repo to the indexer for the first time // parseGitLsTreeOutput parses the output of a `git ls-tree -r --full-name` command
func genesisChanges(repo *Repository) (*repoChanges, error) { func parseGitLsTreeOutput(stdout string) ([]fileUpdate, error) {
var changes repoChanges lines := strings.Split(stdout, "\n")
stdout, err := git.NewCommand("ls-files").RunInDir(repo.LocalCopyPath()) updates := make([]fileUpdate, 0, len(lines))
if err != nil { for _, line := range lines {
return nil, err // expect line to be "<mode> <object-type> <object-sha>\t<filename>"
} line = strings.TrimSpace(line)
for _, line := range strings.Split(stdout, "\n") { if len(line) == 0 {
filename := strings.TrimSpace(line)
if len(filename) == 0 {
continue continue
} else if filename[0] == '"' { }
firstSpaceIndex := strings.IndexByte(line, ' ')
if firstSpaceIndex < 0 {
log.Error(4, "Misformatted git ls-tree output: %s", line)
continue
}
tabIndex := strings.IndexByte(line, '\t')
if tabIndex < 42+firstSpaceIndex || tabIndex == len(line)-1 {
log.Error(4, "Misformatted git ls-tree output: %s", line)
continue
}
if objectType := line[firstSpaceIndex+1 : tabIndex-41]; objectType != "blob" {
// submodules appear as commit objects, we do not index submodules
continue
}
blobSha := line[tabIndex-40 : tabIndex]
filename := line[tabIndex+1:]
if filename[0] == '"' {
var err error
filename, err = strconv.Unquote(filename) filename, err = strconv.Unquote(filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
changes.UpdatedFiles = append(changes.UpdatedFiles, filename) updates = append(updates, fileUpdate{
Filename: filename,
BlobSha: blobSha,
})
} }
return &changes, nil return updates, nil
}
// genesisChanges get changes to add repo to the indexer for the first time
func genesisChanges(repo *Repository, revision string) (*repoChanges, error) {
var changes repoChanges
stdout, err := git.NewCommand("ls-tree", "--full-tree", "-r", revision).
RunInDir(repo.RepoPath())
if err != nil {
return nil, err
}
changes.Updates, err = parseGitLsTreeOutput(stdout)
return &changes, err
} }
// nonGenesisChanges get changes since the previous indexer update // nonGenesisChanges get changes since the previous indexer update
func nonGenesisChanges(repo *Repository) (*repoChanges, error) { func nonGenesisChanges(repo *Repository, revision string) (*repoChanges, error) {
diffCmd := git.NewCommand("diff", "--name-status", diffCmd := git.NewCommand("diff", "--name-status",
repo.IndexerStatus.CommitSha, "HEAD") repo.IndexerStatus.CommitSha, revision)
stdout, err := diffCmd.RunInDir(repo.LocalCopyPath()) stdout, err := diffCmd.RunInDir(repo.RepoPath())
if err != nil { if err != nil {
// previous commit sha may have been removed by a force push, so // previous commit sha may have been removed by a force push, so
// try rebuilding from scratch // try rebuilding from scratch
log.Warn("git diff: %v", err)
if err = indexer.DeleteRepoFromIndexer(repo.ID); err != nil { if err = indexer.DeleteRepoFromIndexer(repo.ID); err != nil {
return nil, err return nil, err
} }
return genesisChanges(repo) return genesisChanges(repo, revision)
} }
var changes repoChanges var changes repoChanges
updatedFilenames := make([]string, 0, 10)
for _, line := range strings.Split(stdout, "\n") { for _, line := range strings.Split(stdout, "\n") {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if len(line) == 0 { if len(line) == 0 {
@@ -274,23 +313,22 @@ func nonGenesisChanges(repo *Repository) (*repoChanges, error) {
switch status := line[0]; status { switch status := line[0]; status {
case 'M', 'A': case 'M', 'A':
changes.UpdatedFiles = append(changes.UpdatedFiles, filename) updatedFilenames = append(updatedFilenames, filename)
case 'D': case 'D':
changes.RemovedFiles = append(changes.RemovedFiles, filename) changes.RemovedFilenames = append(changes.RemovedFilenames, filename)
default: default:
log.Warn("Unrecognized status: %c (line=%s)", status, line) log.Warn("Unrecognized status: %c (line=%s)", status, line)
} }
} }
return &changes, nil
}
func updateLastIndexSync(repo *Repository) error { cmd := git.NewCommand("ls-tree", "--full-tree", revision, "--")
stdout, err := git.NewCommand("rev-parse", "HEAD").RunInDir(repo.LocalCopyPath()) cmd.AddArguments(updatedFilenames...)
stdout, err = cmd.RunInDir(repo.RepoPath())
if err != nil { if err != nil {
return err return nil, err
} }
sha := strings.TrimSpace(stdout) changes.Updates, err = parseGitLsTreeOutput(stdout)
return repo.updateIndexerStatus(sha) return &changes, err
} }
func processRepoIndexerOperationQueue() { func processRepoIndexerOperationQueue() {

View File

@@ -244,6 +244,8 @@ func MirrorUpdate() {
// SyncMirrors checks and syncs mirrors. // SyncMirrors checks and syncs mirrors.
// TODO: sync more mirrors at same time. // TODO: sync more mirrors at same time.
func SyncMirrors() { func SyncMirrors() {
sess := x.NewSession()
defer sess.Close()
// Start listening on new sync requests. // Start listening on new sync requests.
for repoID := range MirrorQueue.Queue() { for repoID := range MirrorQueue.Queue() {
log.Trace("SyncMirrors [repo_id: %v]", repoID) log.Trace("SyncMirrors [repo_id: %v]", repoID)
@@ -260,10 +262,22 @@ func SyncMirrors() {
} }
m.ScheduleNextUpdate() m.ScheduleNextUpdate()
if err = UpdateMirror(m); err != nil { if err = updateMirror(sess, m); err != nil {
log.Error(4, "UpdateMirror [%s]: %v", repoID, err) log.Error(4, "UpdateMirror [%s]: %v", repoID, err)
continue continue
} }
// Get latest commit date and update to current repository updated time
commitDate, err := git.GetLatestCommitTime(m.Repo.RepoPath())
if err != nil {
log.Error(2, "GetLatestCommitDate [%s]: %v", m.RepoID, err)
continue
}
if _, err = sess.Exec("UPDATE repository SET updated_unix = ? WHERE id = ?", commitDate.Unix(), m.RepoID); err != nil {
log.Error(2, "Update repository 'updated_unix' [%s]: %v", m.RepoID, err)
continue
}
} }
} }

View File

@@ -9,6 +9,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@@ -72,6 +73,18 @@ func createTestEngine(fixturesDir string) error {
return InitFixtures(&testfixtures.SQLite{}, fixturesDir) return InitFixtures(&testfixtures.SQLite{}, fixturesDir)
} }
func removeAllWithRetry(dir string) error {
var err error
for i := 0; i < 20; i++ {
err = os.RemoveAll(dir)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
return err
}
// PrepareTestDatabase load test fixtures into test database // PrepareTestDatabase load test fixtures into test database
func PrepareTestDatabase() error { func PrepareTestDatabase() error {
return LoadFixtures() return LoadFixtures()
@@ -81,7 +94,7 @@ func PrepareTestDatabase() error {
// by tests that use the above MainTest(..) function. // by tests that use the above MainTest(..) function.
func PrepareTestEnv(t testing.TB) { func PrepareTestEnv(t testing.TB) {
assert.NoError(t, PrepareTestDatabase()) assert.NoError(t, PrepareTestDatabase())
assert.NoError(t, os.RemoveAll(setting.RepoRootPath)) assert.NoError(t, removeAllWithRetry(setting.RepoRootPath))
metaPath := filepath.Join(giteaRoot, "integrations", "gitea-repositories-meta") metaPath := filepath.Join(giteaRoot, "integrations", "gitea-repositories-meta")
assert.NoError(t, com.CopyDir(metaPath, setting.RepoRootPath)) assert.NoError(t, com.CopyDir(metaPath, setting.RepoRootPath))
} }

View File

@@ -299,7 +299,9 @@ func (u *User) generateRandomAvatar(e Engine) error {
} }
// NOTICE for random avatar, it still uses id as avatar name, but custom avatar use md5 // NOTICE for random avatar, it still uses id as avatar name, but custom avatar use md5
// since random image is not a user's photo, there is no security for enumable // since random image is not a user's photo, there is no security for enumable
u.Avatar = fmt.Sprintf("%d", u.ID) if u.Avatar == "" {
u.Avatar = fmt.Sprintf("%d", u.ID)
}
if err = os.MkdirAll(filepath.Dir(u.CustomAvatarPath()), os.ModePerm); err != nil { if err = os.MkdirAll(filepath.Dir(u.CustomAvatarPath()), os.ModePerm); err != nil {
return fmt.Errorf("MkdirAll: %v", err) return fmt.Errorf("MkdirAll: %v", err)
} }

View File

@@ -45,7 +45,7 @@ func WikiNameToFilename(name string) string {
// WikiFilenameToName converts a wiki filename to its corresponding page name. // WikiFilenameToName converts a wiki filename to its corresponding page name.
func WikiFilenameToName(filename string) (string, error) { func WikiFilenameToName(filename string) (string, error) {
if !strings.HasSuffix(filename, ".md") { if !strings.HasSuffix(filename, ".md") {
return "", fmt.Errorf("Invalid wiki filename: %s", filename) return "", ErrWikiInvalidFileName{filename}
} }
basename := filename[:len(filename)-3] basename := filename[:len(filename)-3]
unescaped, err := url.QueryUnescape(basename) unescaped, err := url.QueryUnescape(basename)

View File

@@ -77,11 +77,14 @@ func TestWikiFilenameToName(t *testing.T) {
for _, badFilename := range []string{ for _, badFilename := range []string{
"nofileextension", "nofileextension",
"wrongfileextension.txt", "wrongfileextension.txt",
"badescaping%%.md",
} { } {
_, err := WikiFilenameToName(badFilename) _, err := WikiFilenameToName(badFilename)
assert.Error(t, err) assert.Error(t, err)
assert.True(t, IsErrWikiInvalidFileName(err))
} }
_, err := WikiFilenameToName("badescaping%%.md")
assert.Error(t, err)
assert.False(t, IsErrWikiInvalidFileName(err))
} }
func TestWikiNameToFilenameToName(t *testing.T) { func TestWikiNameToFilenameToName(t *testing.T) {

View File

@@ -9,6 +9,7 @@ import (
"html/template" "html/template"
"io" "io"
"net/http" "net/http"
"net/url"
"path" "path"
"strings" "strings"
"time" "time"
@@ -75,6 +76,26 @@ func (ctx *Context) HasValue(name string) bool {
return ok return ok
} }
// RedirectToFirst redirects to first not empty URL
func (ctx *Context) RedirectToFirst(location ...string) {
for _, loc := range location {
if len(loc) == 0 {
continue
}
u, err := url.Parse(loc)
if err != nil || (u.Scheme != "" && !strings.HasPrefix(strings.ToLower(loc), strings.ToLower(setting.AppURL))) {
continue
}
ctx.Redirect(loc)
return
}
ctx.Redirect(setting.AppSubURL + "/")
return
}
// HTML calls Context.HTML and converts template name to string. // HTML calls Context.HTML and converts template name to string.
func (ctx *Context) HTML(status int, name base.TplName) { func (ctx *Context) HTML(status int, name base.TplName) {
log.Debug("Template: %s", name) log.Debug("Template: %s", name)

View File

@@ -391,7 +391,11 @@ func RenderShortLinks(rawBytes []byte, urlPrefix string, noLink bool, isWikiMark
} }
absoluteLink := isLink([]byte(link)) absoluteLink := isLink([]byte(link))
if !absoluteLink { if !absoluteLink {
link = strings.Replace(link, " ", "+", -1) if image {
link = strings.Replace(link, " ", "+", -1)
} else {
link = strings.Replace(link, " ", "-", -1)
}
} }
if image { if image {
if !absoluteLink { if !absoluteLink {

View File

@@ -10,6 +10,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"html"
"html/template" "html/template"
"mime" "mime"
"net/url" "net/url"
@@ -179,6 +180,7 @@ func NewFuncMap() []template.FuncMap {
return dict, nil return dict, nil
}, },
"Printf": fmt.Sprintf, "Printf": fmt.Sprintf,
"Escape": Escape,
}} }}
} }
@@ -197,6 +199,11 @@ func Str2html(raw string) template.HTML {
return template.HTML(markup.Sanitize(raw)) return template.HTML(markup.Sanitize(raw))
} }
// Escape escapes a HTML string
func Escape(raw string) string {
return html.EscapeString(raw)
}
// List traversings the list // List traversings the list
func List(l *list.List) chan interface{} { func List(l *list.List) chan interface{} {
e := l.Front() e := l.Front()

View File

@@ -307,11 +307,7 @@ func Action(ctx *context.Context) {
return return
} }
redirectTo := ctx.Query("redirect_to") ctx.RedirectToFirst(ctx.Query("redirect_to"), ctx.Repo.RepoLink)
if len(redirectTo) == 0 {
redirectTo = ctx.Repo.RepoLink
}
ctx.Redirect(redirectTo)
} }
// Download download an archive of a repository // Download download an archive of a repository

View File

@@ -128,6 +128,9 @@ func renderWikiPage(ctx *context.Context, isViewPage bool) (*git.Repository, *gi
} }
wikiName, err := models.WikiFilenameToName(entry.Name()) wikiName, err := models.WikiFilenameToName(entry.Name())
if err != nil { if err != nil {
if models.IsErrWikiInvalidFileName(err) {
continue
}
ctx.ServerError("WikiFilenameToName", err) ctx.ServerError("WikiFilenameToName", err)
return nil, nil return nil, nil
} else if wikiName == "_Sidebar" || wikiName == "_Footer" { } else if wikiName == "_Sidebar" || wikiName == "_Footer" {
@@ -262,6 +265,9 @@ func WikiPages(ctx *context.Context) {
} }
wikiName, err := models.WikiFilenameToName(entry.Name()) wikiName, err := models.WikiFilenameToName(entry.Name())
if err != nil { if err != nil {
if models.IsErrWikiInvalidFileName(err) {
continue
}
ctx.ServerError("WikiFilenameToName", err) ctx.ServerError("WikiFilenameToName", err)
return return
} }

View File

@@ -7,36 +7,52 @@ package repo
import ( import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"path/filepath"
"testing" "testing"
"code.gitea.io/git"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"github.com/Unknwon/com"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
const content = "Wiki contents for unit tests" const content = "Wiki contents for unit tests"
const message = "Wiki commit message for unit tests" const message = "Wiki commit message for unit tests"
func wikiPath(repo *models.Repository, wikiName string) string { func wikiEntry(t *testing.T, repo *models.Repository, wikiName string) *git.TreeEntry {
return filepath.Join(repo.LocalWikiPath(), models.WikiNameToFilename(wikiName)) wikiRepo, err := git.OpenRepository(repo.WikiPath())
assert.NoError(t, err)
commit, err := wikiRepo.GetBranchCommit("master")
assert.NoError(t, err)
entries, err := commit.ListEntries()
assert.NoError(t, err)
for _, entry := range entries {
if entry.Name() == models.WikiNameToFilename(wikiName) {
return entry
}
}
return nil
} }
func wikiContent(t *testing.T, repo *models.Repository, wikiName string) string { func wikiContent(t *testing.T, repo *models.Repository, wikiName string) string {
bytes, err := ioutil.ReadFile(wikiPath(repo, wikiName)) entry := wikiEntry(t, repo, wikiName)
if !assert.NotNil(t, entry) {
return ""
}
reader, err := entry.Blob().Data()
assert.NoError(t, err)
bytes, err := ioutil.ReadAll(reader)
assert.NoError(t, err) assert.NoError(t, err)
return string(bytes) return string(bytes)
} }
func assertWikiExists(t *testing.T, repo *models.Repository, wikiName string) { func assertWikiExists(t *testing.T, repo *models.Repository, wikiName string) {
assert.True(t, com.IsExist(wikiPath(repo, wikiName))) assert.NotNil(t, wikiEntry(t, repo, wikiName))
} }
func assertWikiNotExists(t *testing.T, repo *models.Repository, wikiName string) { func assertWikiNotExists(t *testing.T, repo *models.Repository, wikiName string) {
assert.False(t, com.IsExist(wikiPath(repo, wikiName))) assert.Nil(t, wikiEntry(t, repo, wikiName))
} }
func assertPagesMetas(t *testing.T, expectedNames []string, metas interface{}) { func assertPagesMetas(t *testing.T, expectedNames []string, metas interface{}) {

View File

@@ -93,12 +93,8 @@ func checkAutoLogin(ctx *context.Context) bool {
} }
if isSucceed { if isSucceed {
if len(redirectTo) > 0 { ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL)
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL) ctx.RedirectToFirst(redirectTo, setting.AppSubURL+string(setting.LandingPageURL))
ctx.Redirect(redirectTo)
} else {
ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
}
return true return true
} }
@@ -350,7 +346,7 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR
if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 { if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL) ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL)
if obeyRedirect { if obeyRedirect {
ctx.Redirect(redirectTo) ctx.RedirectToFirst(redirectTo)
} }
return return
} }
@@ -439,7 +435,7 @@ func handleOAuth2SignIn(u *models.User, gothUser goth.User, ctx *context.Context
if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 { if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL) ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL)
ctx.Redirect(redirectTo) ctx.RedirectToFirst(redirectTo)
return return
} }

View File

@@ -49,12 +49,8 @@ func SignInOpenID(ctx *context.Context) {
} }
if isSucceed { if isSucceed {
if len(redirectTo) > 0 { ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL)
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL) ctx.RedirectToFirst(redirectTo)
ctx.Redirect(redirectTo)
} else {
ctx.Redirect(setting.AppSubURL + "/")
}
return return
} }

View File

@@ -66,12 +66,14 @@ func retrieveFeeds(ctx *context.Context, options models.GetFeedsOptions) {
if ctx.User != nil { if ctx.User != nil {
userCache[ctx.User.ID] = ctx.User userCache[ctx.User.ID] = ctx.User
} }
repoCache := map[int64]*models.Repository{}
for _, act := range actions { for _, act := range actions {
// Cache results to reduce queries. if act.ActUser != nil {
u, ok := userCache[act.ActUserID] userCache[act.ActUserID] = act.ActUser
}
repoOwner, ok := userCache[act.Repo.OwnerID]
if !ok { if !ok {
u, err = models.GetUserByID(act.ActUserID) repoOwner, err = models.GetUserByID(act.Repo.OwnerID)
if err != nil { if err != nil {
if models.IsErrUserNotExist(err) { if models.IsErrUserNotExist(err) {
continue continue
@@ -79,35 +81,9 @@ func retrieveFeeds(ctx *context.Context, options models.GetFeedsOptions) {
ctx.ServerError("GetUserByID", err) ctx.ServerError("GetUserByID", err)
return return
} }
userCache[act.ActUserID] = u userCache[repoOwner.ID] = repoOwner
} }
act.ActUser = u act.Repo.Owner = repoOwner
repo, ok := repoCache[act.RepoID]
if !ok {
repo, err = models.GetRepositoryByID(act.RepoID)
if err != nil {
if models.IsErrRepoNotExist(err) {
continue
}
ctx.ServerError("GetRepositoryByID", err)
return
}
}
act.Repo = repo
repoOwner, ok := userCache[repo.OwnerID]
if !ok {
repoOwner, err = models.GetUserByID(repo.OwnerID)
if err != nil {
if models.IsErrUserNotExist(err) {
continue
}
ctx.ServerError("GetUserByID", err)
return
}
}
repo.Owner = repoOwner
} }
ctx.Data["Feeds"] = actions ctx.Data["Feeds"] = actions
} }
@@ -154,7 +130,8 @@ func Dashboard(ctx *context.Context) {
ctx.Data["MirrorCount"] = len(mirrors) ctx.Data["MirrorCount"] = len(mirrors)
ctx.Data["Mirrors"] = mirrors ctx.Data["Mirrors"] = mirrors
retrieveFeeds(ctx, models.GetFeedsOptions{RequestedUser: ctxUser, retrieveFeeds(ctx, models.GetFeedsOptions{
RequestedUser: ctxUser,
IncludePrivate: true, IncludePrivate: true,
OnlyPerformedBy: false, OnlyPerformedBy: false,
IncludeDeleted: false, IncludeDeleted: false,

View File

@@ -271,9 +271,5 @@ func Action(ctx *context.Context) {
return return
} }
redirectTo := ctx.Query("redirect_to") ctx.RedirectToFirst(ctx.Query("redirect_to"), u.HomeLink())
if len(redirectTo) == 0 {
redirectTo = u.HomeLink()
}
ctx.Redirect(redirectTo)
} }

View File

@@ -134,7 +134,7 @@
<p class="desc"> <p class="desc">
<div class="ui red label">{{$.i18n.Tr "repo.activity.closed_issue_label"}}</div> <div class="ui red label">{{$.i18n.Tr "repo.activity.closed_issue_label"}}</div>
#{{.Index}} <a class="title has-emoji" href="{{$.Repository.HTMLURL}}/issues/{{.Index}}">{{.Title}}</a> #{{.Index}} <a class="title has-emoji" href="{{$.Repository.HTMLURL}}/issues/{{.Index}}">{{.Title}}</a>
{{TimeSinceUnix .UpdatedUnix $.Lang}} {{TimeSinceUnix .ClosedUnix $.Lang}}
</p> </p>
{{end}} {{end}}
</div> </div>

View File

@@ -47,9 +47,9 @@
</div> </div>
<div class="text small"> <div class="text small">
{{if .IsViewBranch}} {{if .IsViewBranch}}
{{.i18n.Tr "repo.branch.create_from" .BranchName | Safe}} {{.i18n.Tr "repo.branch.create_from" .BranchName}}
{{else}} {{else}}
{{.i18n.Tr "repo.branch.create_from" (ShortSha .BranchName) | Safe}} {{.i18n.Tr "repo.branch.create_from" (ShortSha .BranchName)}}
{{end}} {{end}}
</div> </div>
</a> </a>

View File

@@ -103,7 +103,7 @@
<img src="{{.Poster.RelAvatarLink}}"> <img src="{{.Poster.RelAvatarLink}}">
</a> </a>
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> <span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a>
{{if .Content}}{{$.i18n.Tr "repo.issues.add_label_at" .Label.ForegroundColor .Label.Color .Label.Name $createdStr | Safe}}{{else}}{{$.i18n.Tr "repo.issues.remove_label_at" .Label.ForegroundColor .Label.Color .Label.Name $createdStr | Safe}}{{end}}</span> {{if .Content}}{{$.i18n.Tr "repo.issues.add_label_at" .Label.ForegroundColor .Label.Color (.Label.Name|Escape) $createdStr | Safe}}{{else}}{{$.i18n.Tr "repo.issues.remove_label_at" .Label.ForegroundColor .Label.Color (.Label.Name|Escape) $createdStr | Safe}}{{end}}</span>
</div> </div>
{{end}} {{end}}
{{else if eq .Type 8}} {{else if eq .Type 8}}
@@ -113,7 +113,7 @@
<img src="{{.Poster.RelAvatarLink}}"> <img src="{{.Poster.RelAvatarLink}}">
</a> </a>
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> <span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a>
{{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{$.i18n.Tr "repo.issues.change_milestone_at" .OldMilestone.Name .Milestone.Name $createdStr | Safe}}{{else}}{{$.i18n.Tr "repo.issues.remove_milestone_at" .OldMilestone.Name $createdStr | Safe}}{{end}}{{else if gt .MilestoneID 0}}{{$.i18n.Tr "repo.issues.add_milestone_at" .Milestone.Name $createdStr | Safe}}{{end}}</span> {{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{$.i18n.Tr "repo.issues.change_milestone_at" (.OldMilestone.Name|Escape) (.Milestone.Name|Escape) $createdStr | Safe}}{{else}}{{$.i18n.Tr "repo.issues.remove_milestone_at" (.OldMilestone.Name|Escape) $createdStr | Safe}}{{end}}{{else if gt .MilestoneID 0}}{{$.i18n.Tr "repo.issues.add_milestone_at" (.Milestone.Name|Escape) $createdStr | Safe}}{{end}}</span>
</div> </div>
{{else if eq .Type 9}} {{else if eq .Type 9}}
<div class="event"> <div class="event">
@@ -131,23 +131,23 @@
{{else if eq .Type 10}} {{else if eq .Type 10}}
<div class="event"> <div class="event">
<span class="octicon octicon-primitive-dot"></span> <span class="octicon octicon-primitive-dot"></span>
<a class="ui avatar image" href="{{.Poster.HomeLink}}">
<img src="{{.Poster.RelAvatarLink}}">
</a>
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a>
{{$.i18n.Tr "repo.issues.change_title_at" (.OldTitle|Escape) (.NewTitle|Escape) $createdStr | Safe}}
</span>
</div> </div>
<a class="ui avatar image" href="{{.Poster.HomeLink}}">
<img src="{{.Poster.RelAvatarLink}}">
</a>
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a>
{{$.i18n.Tr "repo.issues.change_title_at" .OldTitle .NewTitle $createdStr | Safe}}
</span>
{{else if eq .Type 11}} {{else if eq .Type 11}}
<div class="event"> <div class="event">
<span class="octicon octicon-primitive-dot"></span> <span class="octicon octicon-primitive-dot"></span>
<a class="ui avatar image" href="{{.Poster.HomeLink}}">
<img src="{{.Poster.RelAvatarLink}}">
</a>
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a>
{{$.i18n.Tr "repo.issues.delete_branch_at" .CommitSHA $createdStr | Safe}}
</span>
</div> </div>
<a class="ui avatar image" href="{{.Poster.HomeLink}}">
<img src="{{.Poster.RelAvatarLink}}">
</a>
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a>
{{$.i18n.Tr "repo.issues.delete_branch_at" .CommitSHA $createdStr | Safe}}
</span>
{{else if eq .Type 12}} {{else if eq .Type 12}}
<div class="event"> <div class="event">
<span class="octicon octicon-primitive-dot"></span> <span class="octicon octicon-primitive-dot"></span>

View File

@@ -14,7 +14,7 @@
</div> </div>
{{if .Keyword}} {{if .Keyword}}
<h3> <h3>
{{.i18n.Tr "repo.search.results" .Keyword .RepoLink .RepoName | Str2html}} {{.i18n.Tr "repo.search.results" (.Keyword|Escape) .RepoLink .RepoName | Str2html }}
</h3> </h3>
<div class="repository search"> <div class="repository search">
{{range $result := .SearchResults}} {{range $result := .SearchResults}}

View File

@@ -104,7 +104,7 @@
{{.i18n.Tr "repo.wiki.delete_page_button"}} {{.i18n.Tr "repo.wiki.delete_page_button"}}
</div> </div>
<div class="content"> <div class="content">
<p>{{.i18n.Tr "repo.wiki.delete_page_notice_1" $title | Safe}}</p> <p>{{.i18n.Tr "repo.wiki.delete_page_notice_1" ($title|Escape) | Safe}}</p>
</div> </div>
{{template "base/delete_modal_actions" .}} {{template "base/delete_modal_actions" .}}
</div> </div>

View File

@@ -8,6 +8,10 @@ protocol providers, as long as they implement the `Provider` and `Session` inter
This package was inspired by [https://github.com/intridea/omniauth](https://github.com/intridea/omniauth). This package was inspired by [https://github.com/intridea/omniauth](https://github.com/intridea/omniauth).
## Goth Needs a New Maintainer
[https://blog.gobuffalo.io/goth-needs-a-new-maintainer-626cd47ca37b](https://blog.gobuffalo.io/goth-needs-a-new-maintainer-626cd47ca37b) - TL;DR: I, @markbates, won't be responding to any more issues, PRs, etc... for this package. A new maintainer needs to be found ASAP. Is this you?
## Installation ## Installation
```text ```text
@@ -18,6 +22,8 @@ $ go get github.com/markbates/goth
* Amazon * Amazon
* Auth0 * Auth0
* Azure AD
* Battle.net
* Bitbucket * Bitbucket
* Box * Box
* Cloud Foundry * Cloud Foundry
@@ -26,6 +32,7 @@ $ go get github.com/markbates/goth
* Digital Ocean * Digital Ocean
* Discord * Discord
* Dropbox * Dropbox
* Eve Online
* Facebook * Facebook
* Fitbit * Fitbit
* GitHub * GitHub
@@ -38,6 +45,7 @@ $ go get github.com/markbates/goth
* Lastfm * Lastfm
* Linkedin * Linkedin
* Meetup * Meetup
* MicrosoftOnline
* OneDrive * OneDrive
* OpenID Connect (auto discovery) * OpenID Connect (auto discovery)
* Paypal * Paypal
@@ -50,7 +58,9 @@ $ go get github.com/markbates/goth
* Twitch * Twitch
* Twitter * Twitter
* Uber * Uber
* VK
* Wepay * Wepay
* Xero
* Yahoo * Yahoo
* Yammer * Yammer
@@ -71,17 +81,51 @@ $ go get github.com/markbates/goth
```text ```text
$ cd goth/examples $ cd goth/examples
$ go get -v $ go get -v
$ go build $ go build
$ ./examples $ ./examples
``` ```
Now open up your browser and go to [http://localhost:3000](http://localhost:3000) to see the example. Now open up your browser and go to [http://localhost:3000](http://localhost:3000) to see the example.
To actually use the different providers, please make sure you configure them given the system environments as defined in the examples/main.go file To actually use the different providers, please make sure you set environment variables. Example given in the examples/main.go file
## Security Notes
By default, gothic uses a `CookieStore` from the `gorilla/sessions` package to store session data.
As configured, this default store (`gothic.Store`) will generate cookies with `Options`:
```go
&Options{
Path: "/",
Domain: "",
MaxAge: 86400 * 30,
HttpOnly: true,
Secure: false,
}
```
To tailor these fields for your application, you can override the `gothic.Store` variable at startup.
The follow snippet show one way to do this:
```go
key := "" // Replace with your SESSION_SECRET or similar
maxAge := 86400 * 30 // 30 days
isProd := false // Set to true when serving over https
store := sessions.NewCookieStore([]byte(key))
store.MaxAge(maxAge)
store.Options.Path = "/"
store.Options.HttpOnly = true // HttpOnly should always be enabled
store.Options.Secure = isProd
gothic.Store = store
```
## Issues ## Issues
Issues always stand a significantly better chance of getting fixed if the are accompanied by a Issues always stand a significantly better chance of getting fixed if they are accompanied by a
pull request. pull request.
## Contributing ## Contributing
@@ -94,50 +138,3 @@ Would I love to see more providers? Certainly! Would you love to contribute one?
4. Commit your changes (git commit -am 'Add some feature') 4. Commit your changes (git commit -am 'Add some feature')
5. Push to the branch (git push origin my-new-feature) 5. Push to the branch (git push origin my-new-feature)
6. Create new Pull Request 6. Create new Pull Request
## Contributors
* Mark Bates
* Tyler Bunnell
* Corey McGrillis
* willemvd
* Rakesh Goyal
* Andy Grunwald
* Glenn Walker
* Kevin Fitzpatrick
* Ben Tranter
* Sharad Ganapathy
* Andrew Chilton
* sharadgana
* Aurorae
* Craig P Jolicoeur
* Zac Bergquist
* Geoff Franks
* Raphael Geronimi
* Noah Shibley
* lumost
* oov
* Felix Lamouroux
* Rafael Quintela
* Tyler
* DenSm
* Samy KACIMI
* dante gray
* Noah
* Jacob Walker
* Marin Martinic
* Roy
* Omni Adams
* Sasa Brankovic
* dkhamsing
* Dante Swift
* Attila Domokos
* Albin Gilles
* Syed Zubairuddin
* Johnny Boursiquot
* Jerome Touffe-Blin
* bryanl
* Masanobu YOSHIOKA
* Jonathan Hall
* HaiMing.Yin
* Sairam Kunala

View File

@@ -8,10 +8,18 @@ See https://github.com/markbates/goth/examples/main.go to see this in action.
package gothic package gothic
import ( import (
"bytes"
"compress/gzip"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"math/rand"
"net/http" "net/http"
"net/url"
"os" "os"
"strings"
"time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
@@ -27,15 +35,21 @@ var defaultStore sessions.Store
var keySet = false var keySet = false
var gothicRand *rand.Rand
func init() { func init() {
key := []byte(os.Getenv("SESSION_SECRET")) key := []byte(os.Getenv("SESSION_SECRET"))
keySet = len(key) != 0 keySet = len(key) != 0
Store = sessions.NewCookieStore([]byte(key))
cookieStore := sessions.NewCookieStore([]byte(key))
cookieStore.Options.HttpOnly = true
Store = cookieStore
defaultStore = Store defaultStore = Store
gothicRand = rand.New(rand.NewSource(time.Now().UnixNano()))
} }
/* /*
BeginAuthHandler is a convienence handler for starting the authentication process. BeginAuthHandler is a convenience handler for starting the authentication process.
It expects to be able to get the name of the provider from the query parameters It expects to be able to get the name of the provider from the query parameters
as either "provider" or ":provider". as either "provider" or ":provider".
@@ -65,8 +79,16 @@ var SetState = func(req *http.Request) string {
return state return state
} }
return "state" // If a state query param is not passed in, generate a random
// base64-encoded nonce so that the state on the auth URL
// is unguessable, preventing CSRF attacks, as described in
//
// https://auth0.com/docs/protocols/oauth2/oauth-state#keep-reading
nonceBytes := make([]byte, 64)
for i := 0; i < 64; i++ {
nonceBytes[i] = byte(gothicRand.Int63() % 256)
}
return base64.URLEncoding.EncodeToString(nonceBytes)
} }
// GetState gets the state returned by the provider during the callback. // GetState gets the state returned by the provider during the callback.
@@ -87,7 +109,6 @@ I would recommend using the BeginAuthHandler instead of doing all of these steps
yourself, but that's entirely up to you. yourself, but that's entirely up to you.
*/ */
func GetAuthURL(res http.ResponseWriter, req *http.Request) (string, error) { func GetAuthURL(res http.ResponseWriter, req *http.Request) (string, error) {
if !keySet && defaultStore == Store { if !keySet && defaultStore == Store {
fmt.Println("goth/gothic: no SESSION_SECRET environment variable is set. The default cookie store is not available and any calls will fail. Ignore this warning if you are using a different store.") fmt.Println("goth/gothic: no SESSION_SECRET environment variable is set. The default cookie store is not available and any calls will fail. Ignore this warning if you are using a different store.")
} }
@@ -111,7 +132,7 @@ func GetAuthURL(res http.ResponseWriter, req *http.Request) (string, error) {
return "", err return "", err
} }
err = storeInSession(providerName, sess.Marshal(), req, res) err = StoreInSession(providerName, sess.Marshal(), req, res)
if err != nil { if err != nil {
return "", err return "", err
@@ -130,7 +151,7 @@ as either "provider" or ":provider".
See https://github.com/markbates/goth/examples/main.go to see this in action. See https://github.com/markbates/goth/examples/main.go to see this in action.
*/ */
var CompleteUserAuth = func(res http.ResponseWriter, req *http.Request) (goth.User, error) { var CompleteUserAuth = func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
defer Logout(res, req)
if !keySet && defaultStore == Store { if !keySet && defaultStore == Store {
fmt.Println("goth/gothic: no SESSION_SECRET environment variable is set. The default cookie store is not available and any calls will fail. Ignore this warning if you are using a different store.") fmt.Println("goth/gothic: no SESSION_SECRET environment variable is set. The default cookie store is not available and any calls will fail. Ignore this warning if you are using a different store.")
} }
@@ -145,7 +166,7 @@ var CompleteUserAuth = func(res http.ResponseWriter, req *http.Request) (goth.Us
return goth.User{}, err return goth.User{}, err
} }
value, err := getFromSession(providerName, req) value, err := GetFromSession(providerName, req)
if err != nil { if err != nil {
return goth.User{}, err return goth.User{}, err
} }
@@ -155,6 +176,11 @@ var CompleteUserAuth = func(res http.ResponseWriter, req *http.Request) (goth.Us
return goth.User{}, err return goth.User{}, err
} }
err = validateState(req, sess)
if err != nil {
return goth.User{}, err
}
user, err := provider.FetchUser(sess) user, err := provider.FetchUser(sess)
if err == nil { if err == nil {
// user can be found with existing session data // user can be found with existing session data
@@ -167,13 +193,49 @@ var CompleteUserAuth = func(res http.ResponseWriter, req *http.Request) (goth.Us
return goth.User{}, err return goth.User{}, err
} }
err = storeInSession(providerName, sess.Marshal(), req, res) err = StoreInSession(providerName, sess.Marshal(), req, res)
if err != nil { if err != nil {
return goth.User{}, err return goth.User{}, err
} }
return provider.FetchUser(sess) gu, err := provider.FetchUser(sess)
return gu, err
}
// validateState ensures that the state token param from the original
// AuthURL matches the one included in the current (callback) request.
func validateState(req *http.Request, sess goth.Session) error {
rawAuthURL, err := sess.GetAuthURL()
if err != nil {
return err
}
authURL, err := url.Parse(rawAuthURL)
if err != nil {
return err
}
originalState := authURL.Query().Get("state")
if originalState != "" && (originalState != req.URL.Query().Get("state")) {
return errors.New("state token mismatch")
}
return nil
}
// Logout invalidates a user session.
func Logout(res http.ResponseWriter, req *http.Request) error {
session, err := Store.Get(req, SessionName)
if err != nil {
return err
}
session.Options.MaxAge = -1
session.Values = make(map[interface{}]interface{})
err = session.Save(req, res)
if err != nil {
return errors.New("Could not delete user session ")
}
return nil
} }
// GetProviderName is a function used to get the name of a provider // GetProviderName is a function used to get the name of a provider
@@ -184,36 +246,99 @@ var CompleteUserAuth = func(res http.ResponseWriter, req *http.Request) (goth.Us
var GetProviderName = getProviderName var GetProviderName = getProviderName
func getProviderName(req *http.Request) (string, error) { func getProviderName(req *http.Request) (string, error) {
provider := req.URL.Query().Get("provider")
if provider == "" { // get all the used providers
if p, ok := mux.Vars(req)["provider"]; ok { providers := goth.GetProviders()
// loop over the used providers, if we already have a valid session for any provider (ie. user is already logged-in with a provider), then return that provider name
for _, provider := range providers {
p := provider.Name()
session, _ := Store.Get(req, p+SessionName)
value := session.Values[p]
if _, ok := value.(string); ok {
return p, nil return p, nil
} }
} }
if provider == "" {
provider = req.URL.Query().Get(":provider") // try to get it from the url param "provider"
if p := req.URL.Query().Get("provider"); p != "" {
return p, nil
} }
if provider == "" {
return provider, errors.New("you must select a provider") // try to get it from the url param ":provider"
if p := req.URL.Query().Get(":provider"); p != "" {
return p, nil
} }
return provider, nil
// try to get it from the context's value of "provider" key
if p, ok := mux.Vars(req)["provider"]; ok {
return p, nil
}
// try to get it from the go-context's value of "provider" key
if p, ok := req.Context().Value("provider").(string); ok {
return p, nil
}
// if not found then return an empty string with the corresponding error
return "", errors.New("you must select a provider")
} }
func storeInSession(key string, value string, req *http.Request, res http.ResponseWriter) error { // StoreInSession stores a specified key/value pair in the session.
session, _ := Store.Get(req, key + SessionName) func StoreInSession(key string, value string, req *http.Request, res http.ResponseWriter) error {
session, _ := Store.New(req, SessionName)
session.Values[key] = value if err := updateSessionValue(session, key, value); err != nil {
return err
}
return session.Save(req, res) return session.Save(req, res)
} }
func getFromSession(key string, req *http.Request) (string, error) { // GetFromSession retrieves a previously-stored value from the session.
session, _ := Store.Get(req, key + SessionName) // If no value has previously been stored at the specified key, it will return an error.
func GetFromSession(key string, req *http.Request) (string, error) {
value := session.Values[key] session, _ := Store.Get(req, SessionName)
if value == nil { value, err := getSessionValue(session, key)
if err != nil {
return "", errors.New("could not find a matching session for this request") return "", errors.New("could not find a matching session for this request")
} }
return value.(string), nil return value, nil
} }
func getSessionValue(session *sessions.Session, key string) (string, error) {
value := session.Values[key]
if value == nil {
return "", fmt.Errorf("could not find a matching session for this request")
}
rdata := strings.NewReader(value.(string))
r, err := gzip.NewReader(rdata)
if err != nil {
return "", err
}
s, err := ioutil.ReadAll(r)
if err != nil {
return "", err
}
return string(s), nil
}
func updateSessionValue(session *sessions.Session, key, value string) error {
var b bytes.Buffer
gz := gzip.NewWriter(&b)
if _, err := gz.Write([]byte(value)); err != nil {
return err
}
if err := gz.Flush(); err != nil {
return err
}
if err := gz.Close(); err != nil {
return err
}
session.Values[key] = b.String()
return nil
}

View File

@@ -9,9 +9,9 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"fmt"
"github.com/markbates/goth" "github.com/markbates/goth"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"fmt"
) )
const ( const (
@@ -26,10 +26,10 @@ const (
// one manually. // one manually.
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
p := &Provider{ p := &Provider{
ClientKey: clientKey, ClientKey: clientKey,
Secret: secret, Secret: secret,
CallbackURL: callbackURL, CallbackURL: callbackURL,
providerName: "bitbucket", providerName: "bitbucket",
} }
p.config = newConfig(p, scopes) p.config = newConfig(p, scopes)
return p return p
@@ -125,7 +125,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
func userFromReader(reader io.Reader, user *goth.User) error { func userFromReader(reader io.Reader, user *goth.User) error {
u := struct { u := struct {
ID string `json:"uuid"` ID string `json:"uuid"`
Links struct { Links struct {
Avatar struct { Avatar struct {
URL string `json:"href"` URL string `json:"href"`

View File

@@ -2,21 +2,24 @@
package dropbox package dropbox
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"strings" "strings"
"fmt"
"github.com/markbates/goth" "github.com/markbates/goth"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"fmt"
) )
const ( const (
authURL = "https://www.dropbox.com/1/oauth2/authorize" authURL = "https://www.dropbox.com/oauth2/authorize"
tokenURL = "https://api.dropbox.com/1/oauth2/token" tokenURL = "https://api.dropbox.com/oauth2/token"
accountURL = "https://api.dropbox.com/1/account/info" accountURL = "https://api.dropbox.com/2/users/get_current_account"
) )
// Provider is the implementation of `goth.Provider` for accessing Dropbox. // Provider is the implementation of `goth.Provider` for accessing Dropbox.
@@ -24,6 +27,7 @@ type Provider struct {
ClientKey string ClientKey string
Secret string Secret string
CallbackURL string CallbackURL string
AccountURL string
HTTPClient *http.Client HTTPClient *http.Client
config *oauth2.Config config *oauth2.Config
providerName string providerName string
@@ -40,10 +44,11 @@ type Session struct {
// create one manually. // create one manually.
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
p := &Provider{ p := &Provider{
ClientKey: clientKey, ClientKey: clientKey,
Secret: secret, Secret: secret,
CallbackURL: callbackURL, CallbackURL: callbackURL,
providerName: "dropbox", AccountURL: accountURL,
providerName: "dropbox",
} }
p.config = newConfig(p, scopes) p.config = newConfig(p, scopes)
return p return p
@@ -86,7 +91,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
} }
req, err := http.NewRequest("GET", accountURL, nil) req, err := http.NewRequest("POST", p.AccountURL, nil)
if err != nil { if err != nil {
return user, err return user, err
} }
@@ -101,7 +106,17 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode) return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode)
} }
err = userFromReader(resp.Body, &user) bits, err := ioutil.ReadAll(resp.Body)
if err != nil {
return user, err
}
err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData)
if err != nil {
return user, err
}
err = userFromReader(bytes.NewReader(bits), &user)
return user, err return user, err
} }
@@ -161,22 +176,29 @@ func newConfig(p *Provider, scopes []string) *oauth2.Config {
func userFromReader(r io.Reader, user *goth.User) error { func userFromReader(r io.Reader, user *goth.User) error {
u := struct { u := struct {
Name string `json:"display_name"` AccountID string `json:"account_id"`
NameDetails struct { Name struct {
NickName string `json:"familiar_name"` GivenName string `json:"given_name"`
} `json:"name_details"` Surname string `json:"surname"`
Location string `json:"country"` DisplayName string `json:"display_name"`
Email string `json:"email"` } `json:"name"`
Country string `json:"country"`
Email string `json:"email"`
ProfilePhotoURL string `json:"profile_photo_url"`
}{} }{}
err := json.NewDecoder(r).Decode(&u) err := json.NewDecoder(r).Decode(&u)
if err != nil { if err != nil {
return err return err
} }
user.UserID = u.AccountID // The user's unique Dropbox ID.
user.FirstName = u.Name.GivenName
user.LastName = u.Name.Surname
user.Name = strings.TrimSpace(fmt.Sprintf("%s %s", u.Name.GivenName, u.Name.Surname))
user.Description = u.Name.DisplayName // Full name plus parenthetical team naem
user.Email = u.Email user.Email = u.Email
user.Name = u.Name user.NickName = u.Email // Email is the dropbox username
user.NickName = u.NameDetails.NickName user.Location = u.Country
user.UserID = u.Email // Dropbox doesn't provide a separate user ID user.AvatarURL = u.ProfilePhotoURL // May be blank
user.Location = u.Location
return nil return nil
} }

View File

@@ -11,12 +11,12 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"github.com/markbates/goth"
"golang.org/x/oauth2"
"fmt"
"crypto/hmac" "crypto/hmac"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt"
"github.com/markbates/goth"
"golang.org/x/oauth2"
) )
const ( const (
@@ -30,10 +30,10 @@ const (
// one manually. // one manually.
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
p := &Provider{ p := &Provider{
ClientKey: clientKey, ClientKey: clientKey,
Secret: secret, Secret: secret,
CallbackURL: callbackURL, CallbackURL: callbackURL,
providerName: "facebook", providerName: "facebook",
} }
p.config = newConfig(p, scopes) p.config = newConfig(p, scopes)
return p return p
@@ -129,7 +129,7 @@ func userFromReader(reader io.Reader, user *goth.User) error {
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
Link string `json:"link"` Link string `json:"link"`
Picture struct { Picture struct {
Data struct { Data struct {
URL string `json:"url"` URL string `json:"url"`
} `json:"data"` } `json:"data"`

View File

@@ -11,9 +11,9 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"fmt"
"github.com/markbates/goth" "github.com/markbates/goth"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"fmt"
) )
// These vars define the Authentication, Token, and Profile URLS for Gitlab. If // These vars define the Authentication, Token, and Profile URLS for Gitlab. If

View File

@@ -11,9 +11,9 @@ import (
"net/url" "net/url"
"strings" "strings"
"fmt"
"github.com/markbates/goth" "github.com/markbates/goth"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"fmt"
) )
const ( const (
@@ -27,10 +27,10 @@ const (
// one manually. // one manually.
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
p := &Provider{ p := &Provider{
ClientKey: clientKey, ClientKey: clientKey,
Secret: secret, Secret: secret,
CallbackURL: callbackURL, CallbackURL: callbackURL,
providerName: "gplus", providerName: "gplus",
} }
p.config = newConfig(p, scopes) p.config = newConfig(p, scopes)
return p return p

View File

@@ -1,17 +1,17 @@
package openidConnect package openidConnect
import ( import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/markbates/goth"
"golang.org/x/oauth2"
"io/ioutil"
"net/http" "net/http"
"strings" "strings"
"fmt"
"encoding/json"
"encoding/base64"
"io/ioutil"
"errors"
"golang.org/x/oauth2"
"github.com/markbates/goth"
"time" "time"
"bytes"
) )
const ( const (
@@ -89,14 +89,14 @@ func New(clientKey, secret, callbackURL, openIDAutoDiscoveryURL string, scopes .
Secret: secret, Secret: secret,
CallbackURL: callbackURL, CallbackURL: callbackURL,
UserIdClaims: []string{subjectClaim}, UserIdClaims: []string{subjectClaim},
NameClaims: []string{NameClaim}, NameClaims: []string{NameClaim},
NickNameClaims: []string{NicknameClaim, PreferredUsernameClaim}, NickNameClaims: []string{NicknameClaim, PreferredUsernameClaim},
EmailClaims: []string{EmailClaim}, EmailClaims: []string{EmailClaim},
AvatarURLClaims:[]string{PictureClaim}, AvatarURLClaims: []string{PictureClaim},
FirstNameClaims:[]string{GivenNameClaim}, FirstNameClaims: []string{GivenNameClaim},
LastNameClaims: []string{FamilyNameClaim}, LastNameClaims: []string{FamilyNameClaim},
LocationClaims: []string{AddressClaim}, LocationClaims: []string{AddressClaim},
providerName: "openid-connect", providerName: "openid-connect",
} }

View File

@@ -1,12 +1,12 @@
package openidConnect package openidConnect
import ( import (
"encoding/json"
"errors" "errors"
"github.com/markbates/goth" "github.com/markbates/goth"
"encoding/json" "golang.org/x/oauth2"
"strings" "strings"
"time" "time"
"golang.org/x/oauth2"
) )
// Session stores data during the auth process with the OpenID Connect provider. // Session stores data during the auth process with the OpenID Connect provider.

View File

@@ -9,10 +9,11 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"fmt"
"github.com/markbates/goth" "github.com/markbates/goth"
"github.com/mrjones/oauth" "github.com/mrjones/oauth"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"fmt"
) )
var ( var (
@@ -30,10 +31,10 @@ var (
// If you'd like to use authenticate instead of authorize, use NewAuthenticate instead. // If you'd like to use authenticate instead of authorize, use NewAuthenticate instead.
func New(clientKey, secret, callbackURL string) *Provider { func New(clientKey, secret, callbackURL string) *Provider {
p := &Provider{ p := &Provider{
ClientKey: clientKey, ClientKey: clientKey,
Secret: secret, Secret: secret,
CallbackURL: callbackURL, CallbackURL: callbackURL,
providerName: "twitter", providerName: "twitter",
} }
p.consumer = newConsumer(p, authorizeURL) p.consumer = newConsumer(p, authorizeURL)
return p return p
@@ -43,10 +44,10 @@ func New(clientKey, secret, callbackURL string) *Provider {
// NewAuthenticate uses the authenticate URL instead of the authorize URL. // NewAuthenticate uses the authenticate URL instead of the authorize URL.
func NewAuthenticate(clientKey, secret, callbackURL string) *Provider { func NewAuthenticate(clientKey, secret, callbackURL string) *Provider {
p := &Provider{ p := &Provider{
ClientKey: clientKey, ClientKey: clientKey,
Secret: secret, Secret: secret,
CallbackURL: callbackURL, CallbackURL: callbackURL,
providerName: "twitter", providerName: "twitter",
} }
p.consumer = newConsumer(p, authenticateURL) p.consumer = newConsumer(p, authenticateURL)
return p return p
@@ -107,7 +108,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
response, err := p.consumer.Get( response, err := p.consumer.Get(
endpointProfile, endpointProfile,
map[string]string{"include_entities": "false", "skip_status": "true"}, map[string]string{"include_entities": "false", "skip_status": "true", "include_email": "true"},
sess.AccessToken) sess.AccessToken)
if err != nil { if err != nil {
return user, err return user, err
@@ -126,6 +127,9 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
user.Name = user.RawData["name"].(string) user.Name = user.RawData["name"].(string)
user.NickName = user.RawData["screen_name"].(string) user.NickName = user.RawData["screen_name"].(string)
if user.RawData["email"] != nil {
user.Email = user.RawData["email"].(string)
}
user.Description = user.RawData["description"].(string) user.Description = user.RawData["description"].(string)
user.AvatarURL = user.RawData["profile_image_url"].(string) user.AvatarURL = user.RawData["profile_image_url"].(string)
user.UserID = user.RawData["id_str"].(string) user.UserID = user.RawData["id_str"].(string)

68
vendor/vendor.json vendored
View File

@@ -654,64 +654,74 @@
"revisionTime": "2017-10-25T03:15:54Z" "revisionTime": "2017-10-25T03:15:54Z"
}, },
{ {
"checksumSHA1": "O3KUfEXQPfdQ+tCMpP2RAIRJJqY=", "checksumSHA1": "q9MD1ienC+kmKq5i51oAktQEV1E=",
"origin": "github.com/go-gitea/goth",
"path": "github.com/markbates/goth", "path": "github.com/markbates/goth",
"revision": "90362394a367f9d77730911973462a53d69662ba", "revision": "3b54d96084a5e11030f19556cf68a6ab5d93ba20",
"revisionTime": "2017-02-23T14:12:10Z" "revisionTime": "2018-03-12T06:32:04Z"
}, },
{ {
"checksumSHA1": "MkFKwLV3icyUo4oP0BgEs+7+R1Y=", "checksumSHA1": "FISfgOkoMtn98wglLUvfBTZ6baE=",
"origin": "github.com/go-gitea/goth/gothic",
"path": "github.com/markbates/goth/gothic", "path": "github.com/markbates/goth/gothic",
"revision": "90362394a367f9d77730911973462a53d69662ba", "revision": "3b54d96084a5e11030f19556cf68a6ab5d93ba20",
"revisionTime": "2017-02-23T14:12:10Z" "revisionTime": "2018-03-12T06:32:04Z"
}, },
{ {
"checksumSHA1": "crNSlQADjX6hcxykON2tFCqY4iw=", "checksumSHA1": "pJ+Cws/TU22K6tZ/ALFOvvH1K5U=",
"origin": "github.com/go-gitea/goth/providers/bitbucket",
"path": "github.com/markbates/goth/providers/bitbucket", "path": "github.com/markbates/goth/providers/bitbucket",
"revision": "90362394a367f9d77730911973462a53d69662ba", "revision": "3b54d96084a5e11030f19556cf68a6ab5d93ba20",
"revisionTime": "2017-02-23T14:12:10Z" "revisionTime": "2018-03-12T06:32:04Z"
}, },
{ {
"checksumSHA1": "1Kp4DKkJNVn135Xg8H4a6CFBNy8=", "checksumSHA1": "XsF5HI4240QHbFXbtWWnGgTsoq8=",
"origin": "github.com/go-gitea/goth/providers/dropbox",
"path": "github.com/markbates/goth/providers/dropbox", "path": "github.com/markbates/goth/providers/dropbox",
"revision": "90362394a367f9d77730911973462a53d69662ba", "revision": "3b54d96084a5e11030f19556cf68a6ab5d93ba20",
"revisionTime": "2017-02-23T14:12:10Z" "revisionTime": "2018-03-12T06:32:04Z"
}, },
{ {
"checksumSHA1": "cGs1da29iOBJh5EAH0icKDbN8CA=", "checksumSHA1": "VzbroIA9R00Ig3iGnOlZLU7d4ls=",
"origin": "github.com/go-gitea/goth/providers/facebook",
"path": "github.com/markbates/goth/providers/facebook", "path": "github.com/markbates/goth/providers/facebook",
"revision": "90362394a367f9d77730911973462a53d69662ba", "revision": "3b54d96084a5e11030f19556cf68a6ab5d93ba20",
"revisionTime": "2017-02-23T14:12:10Z" "revisionTime": "2018-03-12T06:32:04Z"
}, },
{ {
"checksumSHA1": "P6nBZ850aaekpOcoXNdRhK86bH8=", "checksumSHA1": "P6nBZ850aaekpOcoXNdRhK86bH8=",
"origin": "github.com/go-gitea/goth/providers/github",
"path": "github.com/markbates/goth/providers/github", "path": "github.com/markbates/goth/providers/github",
"revision": "90362394a367f9d77730911973462a53d69662ba", "revision": "3b54d96084a5e11030f19556cf68a6ab5d93ba20",
"revisionTime": "2017-02-23T14:12:10Z" "revisionTime": "2018-03-12T06:32:04Z"
}, },
{ {
"checksumSHA1": "o/109paSRy9HqV87gR4zUZMMSzs=", "checksumSHA1": "ld488t+yGoTwtmiCSSggEX4fxVk=",
"origin": "github.com/go-gitea/goth/providers/gitlab",
"path": "github.com/markbates/goth/providers/gitlab", "path": "github.com/markbates/goth/providers/gitlab",
"revision": "90362394a367f9d77730911973462a53d69662ba", "revision": "3b54d96084a5e11030f19556cf68a6ab5d93ba20",
"revisionTime": "2017-02-23T14:12:10Z" "revisionTime": "2018-03-12T06:32:04Z"
}, },
{ {
"checksumSHA1": "cX6kR9y94BWFZvI/7UFrsFsP3FQ=", "checksumSHA1": "qXEulD7vnwY9hFrxh91Pm5YrvTM=",
"origin": "github.com/go-gitea/goth/providers/gplus",
"path": "github.com/markbates/goth/providers/gplus", "path": "github.com/markbates/goth/providers/gplus",
"revision": "90362394a367f9d77730911973462a53d69662ba", "revision": "3b54d96084a5e11030f19556cf68a6ab5d93ba20",
"revisionTime": "2017-02-23T14:12:10Z" "revisionTime": "2018-03-12T06:32:04Z"
}, },
{ {
"checksumSHA1": "sMYKhqAUZXM1+T/TjlMhWh8Vveo=", "checksumSHA1": "wsOBzyp4LKDhfCPmX1LLP7T0S3U=",
"origin": "github.com/go-gitea/goth/providers/openidConnect",
"path": "github.com/markbates/goth/providers/openidConnect", "path": "github.com/markbates/goth/providers/openidConnect",
"revision": "90362394a367f9d77730911973462a53d69662ba", "revision": "3b54d96084a5e11030f19556cf68a6ab5d93ba20",
"revisionTime": "2017-02-23T14:12:10Z" "revisionTime": "2018-03-12T06:32:04Z"
}, },
{ {
"checksumSHA1": "1w0V6jYXaGlEtZcMeYTOAAucvgw=", "checksumSHA1": "o6RqMbbE8QNZhNT9TsAIRMPI8tg=",
"origin": "github.com/go-gitea/goth/providers/twitter",
"path": "github.com/markbates/goth/providers/twitter", "path": "github.com/markbates/goth/providers/twitter",
"revision": "90362394a367f9d77730911973462a53d69662ba", "revision": "3b54d96084a5e11030f19556cf68a6ab5d93ba20",
"revisionTime": "2017-02-23T14:12:10Z" "revisionTime": "2018-03-12T06:32:04Z"
}, },
{ {
"checksumSHA1": "61HNjGetaBoMp8HBOpuEZRSim8g=", "checksumSHA1": "61HNjGetaBoMp8HBOpuEZRSim8g=",