mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 10:44:12 +01:00 
			
		
		
		
	Sync with gigs master branch.
This commit is contained in:
		
							parent
							
								
									ee7bfe2ebe
								
							
						
					
					
						commit
						8952eb1ce0
					
				
							
								
								
									
										464
									
								
								models/git.go
									
									
									
									
									
								
							
							
						
						
									
										464
									
								
								models/git.go
									
									
									
									
									
								
							@ -1,464 +0,0 @@
 | 
			
		||||
// Copyright 2014 The Gogs 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 (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"container/list"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gogits/git"
 | 
			
		||||
 | 
			
		||||
	"github.com/gogits/gogs/modules/base"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// RepoFile represents a file object in git repository.
 | 
			
		||||
type RepoFile struct {
 | 
			
		||||
	*git.TreeEntry
 | 
			
		||||
	Path   string
 | 
			
		||||
	Size   int64
 | 
			
		||||
	Repo   *git.Repository
 | 
			
		||||
	Commit *git.Commit
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LookupBlob returns the content of an object.
 | 
			
		||||
func (file *RepoFile) LookupBlob() (*git.Blob, error) {
 | 
			
		||||
	if file.Repo == nil {
 | 
			
		||||
		return nil, ErrRepoFileNotLoaded
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return file.Repo.LookupBlob(file.Id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetBranches returns all branches of given repository.
 | 
			
		||||
func GetBranches(userName, repoName string) ([]string, error) {
 | 
			
		||||
	repo, err := git.OpenRepository(RepoPath(userName, repoName))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	refs, err := repo.AllReferences()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	brs := make([]string, len(refs))
 | 
			
		||||
	for i, ref := range refs {
 | 
			
		||||
		brs[i] = ref.BranchName()
 | 
			
		||||
	}
 | 
			
		||||
	return brs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTags returns all tags of given repository.
 | 
			
		||||
func GetTags(userName, repoName string) ([]string, error) {
 | 
			
		||||
	repo, err := git.OpenRepository(RepoPath(userName, repoName))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	refs, err := repo.AllTags()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tags := make([]string, len(refs))
 | 
			
		||||
	for i, ref := range refs {
 | 
			
		||||
		tags[i] = ref.Name
 | 
			
		||||
	}
 | 
			
		||||
	return tags, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func IsBranchExist(userName, repoName, branchName string) bool {
 | 
			
		||||
	repo, err := git.OpenRepository(RepoPath(userName, repoName))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return repo.IsBranchExist(branchName)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetTargetFile(userName, repoName, branchName, commitId, rpath string) (*RepoFile, error) {
 | 
			
		||||
	repo, err := git.OpenRepository(RepoPath(userName, repoName))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commit, err := repo.GetCommitOfBranch(branchName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		commit, err = repo.GetCommit(commitId)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parts := strings.Split(path.Clean(rpath), "/")
 | 
			
		||||
 | 
			
		||||
	var entry *git.TreeEntry
 | 
			
		||||
	tree := commit.Tree
 | 
			
		||||
	for i, part := range parts {
 | 
			
		||||
		if i == len(parts)-1 {
 | 
			
		||||
			entry = tree.EntryByName(part)
 | 
			
		||||
			if entry == nil {
 | 
			
		||||
				return nil, ErrRepoFileNotExist
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			tree, err = repo.SubTree(tree, part)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	size, err := repo.ObjectSize(entry.Id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	repoFile := &RepoFile{
 | 
			
		||||
		entry,
 | 
			
		||||
		rpath,
 | 
			
		||||
		size,
 | 
			
		||||
		repo,
 | 
			
		||||
		commit,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return repoFile, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetReposFiles returns a list of file object in given directory of repository.
 | 
			
		||||
// func GetReposFilesOfBranch(userName, repoName, branchName, rpath string) ([]*RepoFile, error) {
 | 
			
		||||
// 	return getReposFiles(userName, repoName, commitId, rpath)
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
// GetReposFiles returns a list of file object in given directory of repository.
 | 
			
		||||
func GetReposFiles(userName, repoName, commitId, rpath string) ([]*RepoFile, error) {
 | 
			
		||||
	return getReposFiles(userName, repoName, commitId, rpath)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getReposFiles(userName, repoName, commitId string, rpath string) ([]*RepoFile, error) {
 | 
			
		||||
	repo, err := git.OpenRepository(RepoPath(userName, repoName))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commit, err := repo.GetCommit(commitId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var repodirs []*RepoFile
 | 
			
		||||
	var repofiles []*RepoFile
 | 
			
		||||
	commit.Tree.Walk(func(dirname string, entry *git.TreeEntry) int {
 | 
			
		||||
		if dirname == rpath {
 | 
			
		||||
			// TODO: size get method shoule be improved
 | 
			
		||||
			size, err := repo.ObjectSize(entry.Id)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return 0
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var cm = commit
 | 
			
		||||
			var i int
 | 
			
		||||
			for {
 | 
			
		||||
				i = i + 1
 | 
			
		||||
				//fmt.Println(".....", i, cm.Id(), cm.ParentCount())
 | 
			
		||||
				if cm.ParentCount() == 0 {
 | 
			
		||||
					break
 | 
			
		||||
				} else if cm.ParentCount() == 1 {
 | 
			
		||||
					pt, _ := repo.SubTree(cm.Parent(0).Tree, dirname)
 | 
			
		||||
					if pt == nil {
 | 
			
		||||
						break
 | 
			
		||||
					}
 | 
			
		||||
					pEntry := pt.EntryByName(entry.Name)
 | 
			
		||||
					if pEntry == nil || !pEntry.Id.Equal(entry.Id) {
 | 
			
		||||
						break
 | 
			
		||||
					} else {
 | 
			
		||||
						cm = cm.Parent(0)
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					var emptyCnt = 0
 | 
			
		||||
					var sameIdcnt = 0
 | 
			
		||||
					var lastSameCm *git.Commit
 | 
			
		||||
					//fmt.Println(".....", cm.ParentCount())
 | 
			
		||||
					for i := 0; i < cm.ParentCount(); i++ {
 | 
			
		||||
						//fmt.Println("parent", i, cm.Parent(i).Id())
 | 
			
		||||
						p := cm.Parent(i)
 | 
			
		||||
						pt, _ := repo.SubTree(p.Tree, dirname)
 | 
			
		||||
						var pEntry *git.TreeEntry
 | 
			
		||||
						if pt != nil {
 | 
			
		||||
							pEntry = pt.EntryByName(entry.Name)
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						//fmt.Println("pEntry", pEntry)
 | 
			
		||||
 | 
			
		||||
						if pEntry == nil {
 | 
			
		||||
							emptyCnt = emptyCnt + 1
 | 
			
		||||
							if emptyCnt+sameIdcnt == cm.ParentCount() {
 | 
			
		||||
								if lastSameCm == nil {
 | 
			
		||||
									goto loop
 | 
			
		||||
								} else {
 | 
			
		||||
									cm = lastSameCm
 | 
			
		||||
									break
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
						} else {
 | 
			
		||||
							//fmt.Println(i, "pEntry", pEntry.Id, "entry", entry.Id)
 | 
			
		||||
							if !pEntry.Id.Equal(entry.Id) {
 | 
			
		||||
								goto loop
 | 
			
		||||
							} else {
 | 
			
		||||
								lastSameCm = cm.Parent(i)
 | 
			
		||||
								sameIdcnt = sameIdcnt + 1
 | 
			
		||||
								if emptyCnt+sameIdcnt == cm.ParentCount() {
 | 
			
		||||
									// TODO: now follow the first parent commit?
 | 
			
		||||
									cm = lastSameCm
 | 
			
		||||
									//fmt.Println("sameId...")
 | 
			
		||||
									break
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		loop:
 | 
			
		||||
 | 
			
		||||
			rp := &RepoFile{
 | 
			
		||||
				entry,
 | 
			
		||||
				path.Join(dirname, entry.Name),
 | 
			
		||||
				size,
 | 
			
		||||
				repo,
 | 
			
		||||
				cm,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if entry.IsFile() {
 | 
			
		||||
				repofiles = append(repofiles, rp)
 | 
			
		||||
			} else if entry.IsDir() {
 | 
			
		||||
				repodirs = append(repodirs, rp)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return 0
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	return append(repodirs, repofiles...), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetCommit(userName, repoName, commitId string) (*git.Commit, error) {
 | 
			
		||||
	repo, err := git.OpenRepository(RepoPath(userName, repoName))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return repo.GetCommit(commitId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCommitsByBranch returns all commits of given branch of repository.
 | 
			
		||||
func GetCommitsByBranch(userName, repoName, branchName string) (*list.List, error) {
 | 
			
		||||
	repo, err := git.OpenRepository(RepoPath(userName, repoName))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	r, err := repo.LookupReference(fmt.Sprintf("refs/heads/%s", branchName))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return r.AllCommits()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCommitsByCommitId returns all commits of given commitId of repository.
 | 
			
		||||
func GetCommitsByCommitId(userName, repoName, commitId string) (*list.List, error) {
 | 
			
		||||
	repo, err := git.OpenRepository(RepoPath(userName, repoName))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	oid, err := git.NewOidFromString(commitId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return repo.CommitsBefore(oid)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Diff line types.
 | 
			
		||||
const (
 | 
			
		||||
	DIFF_LINE_PLAIN = iota + 1
 | 
			
		||||
	DIFF_LINE_ADD
 | 
			
		||||
	DIFF_LINE_DEL
 | 
			
		||||
	DIFF_LINE_SECTION
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	DIFF_FILE_ADD = iota + 1
 | 
			
		||||
	DIFF_FILE_CHANGE
 | 
			
		||||
	DIFF_FILE_DEL
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DiffLine struct {
 | 
			
		||||
	LeftIdx  int
 | 
			
		||||
	RightIdx int
 | 
			
		||||
	Type     int
 | 
			
		||||
	Content  string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d DiffLine) GetType() int {
 | 
			
		||||
	return d.Type
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DiffSection struct {
 | 
			
		||||
	Name  string
 | 
			
		||||
	Lines []*DiffLine
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DiffFile struct {
 | 
			
		||||
	Name               string
 | 
			
		||||
	Addition, Deletion int
 | 
			
		||||
	Type               int
 | 
			
		||||
	Sections           []*DiffSection
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Diff struct {
 | 
			
		||||
	TotalAddition, TotalDeletion int
 | 
			
		||||
	Files                        []*DiffFile
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (diff *Diff) NumFiles() int {
 | 
			
		||||
	return len(diff.Files)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DIFF_HEAD = "diff --git "
 | 
			
		||||
 | 
			
		||||
func ParsePatch(reader io.Reader) (*Diff, error) {
 | 
			
		||||
	scanner := bufio.NewScanner(reader)
 | 
			
		||||
	var (
 | 
			
		||||
		curFile    *DiffFile
 | 
			
		||||
		curSection = &DiffSection{
 | 
			
		||||
			Lines: make([]*DiffLine, 0, 10),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		leftLine, rightLine int
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	diff := &Diff{Files: make([]*DiffFile, 0)}
 | 
			
		||||
	var i int
 | 
			
		||||
	for scanner.Scan() {
 | 
			
		||||
		line := scanner.Text()
 | 
			
		||||
		// fmt.Println(i, line)
 | 
			
		||||
		if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		i = i + 1
 | 
			
		||||
		if line == "" {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if line[0] == ' ' {
 | 
			
		||||
			diffLine := &DiffLine{Type: DIFF_LINE_PLAIN, Content: line, LeftIdx: leftLine, RightIdx: rightLine}
 | 
			
		||||
			leftLine++
 | 
			
		||||
			rightLine++
 | 
			
		||||
			curSection.Lines = append(curSection.Lines, diffLine)
 | 
			
		||||
			continue
 | 
			
		||||
		} else if line[0] == '@' {
 | 
			
		||||
			curSection = &DiffSection{}
 | 
			
		||||
			curFile.Sections = append(curFile.Sections, curSection)
 | 
			
		||||
			ss := strings.Split(line, "@@")
 | 
			
		||||
			diffLine := &DiffLine{Type: DIFF_LINE_SECTION, Content: line}
 | 
			
		||||
			curSection.Lines = append(curSection.Lines, diffLine)
 | 
			
		||||
 | 
			
		||||
			// Parse line number.
 | 
			
		||||
			ranges := strings.Split(ss[len(ss)-2][1:], " ")
 | 
			
		||||
			leftLine, _ = base.StrTo(strings.Split(ranges[0], ",")[0][1:]).Int()
 | 
			
		||||
			rightLine, _ = base.StrTo(strings.Split(ranges[1], ",")[0]).Int()
 | 
			
		||||
			continue
 | 
			
		||||
		} else if line[0] == '+' {
 | 
			
		||||
			curFile.Addition++
 | 
			
		||||
			diff.TotalAddition++
 | 
			
		||||
			diffLine := &DiffLine{Type: DIFF_LINE_ADD, Content: line, RightIdx: rightLine}
 | 
			
		||||
			rightLine++
 | 
			
		||||
			curSection.Lines = append(curSection.Lines, diffLine)
 | 
			
		||||
			continue
 | 
			
		||||
		} else if line[0] == '-' {
 | 
			
		||||
			curFile.Deletion++
 | 
			
		||||
			diff.TotalDeletion++
 | 
			
		||||
			diffLine := &DiffLine{Type: DIFF_LINE_DEL, Content: line, LeftIdx: leftLine}
 | 
			
		||||
			if leftLine > 0 {
 | 
			
		||||
				leftLine++
 | 
			
		||||
			}
 | 
			
		||||
			curSection.Lines = append(curSection.Lines, diffLine)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Get new file.
 | 
			
		||||
		if strings.HasPrefix(line, DIFF_HEAD) {
 | 
			
		||||
			fs := strings.Split(line[len(DIFF_HEAD):], " ")
 | 
			
		||||
			a := fs[0]
 | 
			
		||||
 | 
			
		||||
			curFile = &DiffFile{
 | 
			
		||||
				Name:     a[strings.Index(a, "/")+1:],
 | 
			
		||||
				Type:     DIFF_FILE_CHANGE,
 | 
			
		||||
				Sections: make([]*DiffSection, 0, 10),
 | 
			
		||||
			}
 | 
			
		||||
			diff.Files = append(diff.Files, curFile)
 | 
			
		||||
 | 
			
		||||
			// Check file diff type.
 | 
			
		||||
			for scanner.Scan() {
 | 
			
		||||
				switch {
 | 
			
		||||
				case strings.HasPrefix(scanner.Text(), "new file"):
 | 
			
		||||
					curFile.Type = DIFF_FILE_ADD
 | 
			
		||||
				case strings.HasPrefix(scanner.Text(), "deleted"):
 | 
			
		||||
					curFile.Type = DIFF_FILE_DEL
 | 
			
		||||
				case strings.HasPrefix(scanner.Text(), "index"):
 | 
			
		||||
					curFile.Type = DIFF_FILE_CHANGE
 | 
			
		||||
				}
 | 
			
		||||
				if curFile.Type > 0 {
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return diff, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetDiff(repoPath, commitid string) (*Diff, error) {
 | 
			
		||||
	repo, err := git.OpenRepository(repoPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commit, err := repo.GetCommit(commitid)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// First commit of repository.
 | 
			
		||||
	if commit.ParentCount() == 0 {
 | 
			
		||||
		rd, wr := io.Pipe()
 | 
			
		||||
		go func() {
 | 
			
		||||
			cmd := exec.Command("git", "show", commitid)
 | 
			
		||||
			cmd.Dir = repoPath
 | 
			
		||||
			cmd.Stdout = wr
 | 
			
		||||
			cmd.Stdin = os.Stdin
 | 
			
		||||
			cmd.Stderr = os.Stderr
 | 
			
		||||
			cmd.Run()
 | 
			
		||||
			wr.Close()
 | 
			
		||||
		}()
 | 
			
		||||
		defer rd.Close()
 | 
			
		||||
		return ParsePatch(rd)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rd, wr := io.Pipe()
 | 
			
		||||
	go func() {
 | 
			
		||||
		cmd := exec.Command("git", "diff", commit.Parent(0).Oid.String(), commitid)
 | 
			
		||||
		cmd.Dir = repoPath
 | 
			
		||||
		cmd.Stdout = wr
 | 
			
		||||
		cmd.Stdin = os.Stdin
 | 
			
		||||
		cmd.Stderr = os.Stderr
 | 
			
		||||
		cmd.Run()
 | 
			
		||||
		wr.Close()
 | 
			
		||||
	}()
 | 
			
		||||
	defer rd.Close()
 | 
			
		||||
	return ParsePatch(rd)
 | 
			
		||||
}
 | 
			
		||||
@ -1,233 +0,0 @@
 | 
			
		||||
// Copyright 2014 Google Inc. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
// Package oauth2 contains Martini handlers to provide
 | 
			
		||||
// user login via an OAuth 2.0 backend.
 | 
			
		||||
package oauth2
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.google.com/p/goauth2/oauth"
 | 
			
		||||
	"github.com/go-martini/martini"
 | 
			
		||||
	"github.com/martini-contrib/sessions"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	codeRedirect = 302
 | 
			
		||||
	keyToken     = "oauth2_token"
 | 
			
		||||
	keyNextPage  = "next"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	// Path to handle OAuth 2.0 logins.
 | 
			
		||||
	PathLogin = "/login"
 | 
			
		||||
	// Path to handle OAuth 2.0 logouts.
 | 
			
		||||
	PathLogout = "/logout"
 | 
			
		||||
	// Path to handle callback from OAuth 2.0 backend
 | 
			
		||||
	// to exchange credentials.
 | 
			
		||||
	PathCallback = "/oauth2callback"
 | 
			
		||||
	// Path to handle error cases.
 | 
			
		||||
	PathError = "/oauth2error"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Represents OAuth2 backend options.
 | 
			
		||||
type Options struct {
 | 
			
		||||
	ClientId     string
 | 
			
		||||
	ClientSecret string
 | 
			
		||||
	RedirectURL  string
 | 
			
		||||
	Scopes       []string
 | 
			
		||||
 | 
			
		||||
	AuthUrl  string
 | 
			
		||||
	TokenUrl string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Represents a container that contains
 | 
			
		||||
// user's OAuth 2.0 access and refresh tokens.
 | 
			
		||||
type Tokens interface {
 | 
			
		||||
	Access() string
 | 
			
		||||
	Refresh() string
 | 
			
		||||
	IsExpired() bool
 | 
			
		||||
	ExpiryTime() time.Time
 | 
			
		||||
	ExtraData() map[string]string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type token struct {
 | 
			
		||||
	oauth.Token
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *token) ExtraData() map[string]string {
 | 
			
		||||
	return t.Extra
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Returns the access token.
 | 
			
		||||
func (t *token) Access() string {
 | 
			
		||||
	return t.AccessToken
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Returns the refresh token.
 | 
			
		||||
func (t *token) Refresh() string {
 | 
			
		||||
	return t.RefreshToken
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Returns whether the access token is
 | 
			
		||||
// expired or not.
 | 
			
		||||
func (t *token) IsExpired() bool {
 | 
			
		||||
	if t == nil {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return t.Expired()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Returns the expiry time of the user's
 | 
			
		||||
// access token.
 | 
			
		||||
func (t *token) ExpiryTime() time.Time {
 | 
			
		||||
	return t.Expiry
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Formats tokens into string.
 | 
			
		||||
func (t *token) String() string {
 | 
			
		||||
	return fmt.Sprintf("tokens: %v", t)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Returns a new Google OAuth 2.0 backend endpoint.
 | 
			
		||||
func Google(opts *Options) martini.Handler {
 | 
			
		||||
	opts.AuthUrl = "https://accounts.google.com/o/oauth2/auth"
 | 
			
		||||
	opts.TokenUrl = "https://accounts.google.com/o/oauth2/token"
 | 
			
		||||
	return NewOAuth2Provider(opts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Returns a new Github OAuth 2.0 backend endpoint.
 | 
			
		||||
func Github(opts *Options) martini.Handler {
 | 
			
		||||
	opts.AuthUrl = "https://github.com/login/oauth/authorize"
 | 
			
		||||
	opts.TokenUrl = "https://github.com/login/oauth/access_token"
 | 
			
		||||
	return NewOAuth2Provider(opts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Facebook(opts *Options) martini.Handler {
 | 
			
		||||
	opts.AuthUrl = "https://www.facebook.com/dialog/oauth"
 | 
			
		||||
	opts.TokenUrl = "https://graph.facebook.com/oauth/access_token"
 | 
			
		||||
	return NewOAuth2Provider(opts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Returns a generic OAuth 2.0 backend endpoint.
 | 
			
		||||
func NewOAuth2Provider(opts *Options) martini.Handler {
 | 
			
		||||
	config := &oauth.Config{
 | 
			
		||||
		ClientId:     opts.ClientId,
 | 
			
		||||
		ClientSecret: opts.ClientSecret,
 | 
			
		||||
		RedirectURL:  opts.RedirectURL,
 | 
			
		||||
		Scope:        strings.Join(opts.Scopes, " "),
 | 
			
		||||
		AuthURL:      opts.AuthUrl,
 | 
			
		||||
		TokenURL:     opts.TokenUrl,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	transport := &oauth.Transport{
 | 
			
		||||
		Config:    config,
 | 
			
		||||
		Transport: http.DefaultTransport,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return func(s sessions.Session, c martini.Context, w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		if r.Method == "GET" {
 | 
			
		||||
			switch r.URL.Path {
 | 
			
		||||
			case PathLogin:
 | 
			
		||||
				login(transport, s, w, r)
 | 
			
		||||
			case PathLogout:
 | 
			
		||||
				logout(transport, s, w, r)
 | 
			
		||||
			case PathCallback:
 | 
			
		||||
				handleOAuth2Callback(transport, s, w, r)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tk := unmarshallToken(s)
 | 
			
		||||
		if tk != nil {
 | 
			
		||||
			// check if the access token is expired
 | 
			
		||||
			if tk.IsExpired() && tk.Refresh() == "" {
 | 
			
		||||
				s.Delete(keyToken)
 | 
			
		||||
				tk = nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// Inject tokens.
 | 
			
		||||
		c.MapTo(tk, (*Tokens)(nil))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Handler that redirects user to the login page
 | 
			
		||||
// if user is not logged in.
 | 
			
		||||
// Sample usage:
 | 
			
		||||
// m.Get("/login-required", oauth2.LoginRequired, func() ... {})
 | 
			
		||||
var LoginRequired martini.Handler = func() martini.Handler {
 | 
			
		||||
	return func(s sessions.Session, c martini.Context, w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		token := unmarshallToken(s)
 | 
			
		||||
		if token == nil || token.IsExpired() {
 | 
			
		||||
			next := url.QueryEscape(r.URL.RequestURI())
 | 
			
		||||
			http.Redirect(w, r, PathLogin+"?next="+next, codeRedirect)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}()
 | 
			
		||||
 | 
			
		||||
func login(t *oauth.Transport, s sessions.Session, w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	next := extractPath(r.URL.Query().Get(keyNextPage))
 | 
			
		||||
	if s.Get(keyToken) == nil {
 | 
			
		||||
		// User is not logged in.
 | 
			
		||||
		http.Redirect(w, r, t.Config.AuthCodeURL(next), codeRedirect)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// No need to login, redirect to the next page.
 | 
			
		||||
	http.Redirect(w, r, next, codeRedirect)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func logout(t *oauth.Transport, s sessions.Session, w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	next := extractPath(r.URL.Query().Get(keyNextPage))
 | 
			
		||||
	s.Delete(keyToken)
 | 
			
		||||
	http.Redirect(w, r, next, codeRedirect)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func handleOAuth2Callback(t *oauth.Transport, s sessions.Session, w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	next := extractPath(r.URL.Query().Get("state"))
 | 
			
		||||
	code := r.URL.Query().Get("code")
 | 
			
		||||
	tk, err := t.Exchange(code)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		// Pass the error message, or allow dev to provide its own
 | 
			
		||||
		// error handler.
 | 
			
		||||
		http.Redirect(w, r, PathError, codeRedirect)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// Store the credentials in the session.
 | 
			
		||||
	val, _ := json.Marshal(tk)
 | 
			
		||||
	s.Set(keyToken, val)
 | 
			
		||||
	http.Redirect(w, r, next, codeRedirect)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func unmarshallToken(s sessions.Session) (t *token) {
 | 
			
		||||
	if s.Get(keyToken) == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	data := s.Get(keyToken).([]byte)
 | 
			
		||||
	var tk oauth.Token
 | 
			
		||||
	json.Unmarshal(data, &tk)
 | 
			
		||||
	return &token{tk}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func extractPath(next string) string {
 | 
			
		||||
	n, err := url.Parse(next)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "/"
 | 
			
		||||
	}
 | 
			
		||||
	return n.Path
 | 
			
		||||
}
 | 
			
		||||
@ -1,162 +0,0 @@
 | 
			
		||||
// Copyright 2014 Google Inc. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
package oauth2
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/http/httptest"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-martini/martini"
 | 
			
		||||
	"github.com/martini-contrib/sessions"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Test_LoginRedirect(t *testing.T) {
 | 
			
		||||
	recorder := httptest.NewRecorder()
 | 
			
		||||
	m := martini.New()
 | 
			
		||||
	m.Use(sessions.Sessions("my_session", sessions.NewCookieStore([]byte("secret123"))))
 | 
			
		||||
	m.Use(Google(&Options{
 | 
			
		||||
		ClientId:     "client_id",
 | 
			
		||||
		ClientSecret: "client_secret",
 | 
			
		||||
		RedirectURL:  "refresh_url",
 | 
			
		||||
		Scopes:       []string{"x", "y"},
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	r, _ := http.NewRequest("GET", "/login", nil)
 | 
			
		||||
	m.ServeHTTP(recorder, r)
 | 
			
		||||
 | 
			
		||||
	location := recorder.HeaderMap["Location"][0]
 | 
			
		||||
	if recorder.Code != 302 {
 | 
			
		||||
		t.Errorf("Not being redirected to the auth page.")
 | 
			
		||||
	}
 | 
			
		||||
	if location != "https://accounts.google.com/o/oauth2/auth?access_type=&approval_prompt=&client_id=client_id&redirect_uri=refresh_url&response_type=code&scope=x+y&state=" {
 | 
			
		||||
		t.Errorf("Not being redirected to the right page, %v found", location)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_LoginRedirectAfterLoginRequired(t *testing.T) {
 | 
			
		||||
	recorder := httptest.NewRecorder()
 | 
			
		||||
	m := martini.Classic()
 | 
			
		||||
	m.Use(sessions.Sessions("my_session", sessions.NewCookieStore([]byte("secret123"))))
 | 
			
		||||
	m.Use(Google(&Options{
 | 
			
		||||
		ClientId:     "client_id",
 | 
			
		||||
		ClientSecret: "client_secret",
 | 
			
		||||
		RedirectURL:  "refresh_url",
 | 
			
		||||
		Scopes:       []string{"x", "y"},
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	m.Get("/login-required", LoginRequired, func(tokens Tokens) (int, string) {
 | 
			
		||||
		return 200, tokens.Access()
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	r, _ := http.NewRequest("GET", "/login-required?key=value", nil)
 | 
			
		||||
	m.ServeHTTP(recorder, r)
 | 
			
		||||
 | 
			
		||||
	location := recorder.HeaderMap["Location"][0]
 | 
			
		||||
	if recorder.Code != 302 {
 | 
			
		||||
		t.Errorf("Not being redirected to the auth page.")
 | 
			
		||||
	}
 | 
			
		||||
	if location != "/login?next=%2Flogin-required%3Fkey%3Dvalue" {
 | 
			
		||||
		t.Errorf("Not being redirected to the right page, %v found", location)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_Logout(t *testing.T) {
 | 
			
		||||
	recorder := httptest.NewRecorder()
 | 
			
		||||
	s := sessions.NewCookieStore([]byte("secret123"))
 | 
			
		||||
 | 
			
		||||
	m := martini.Classic()
 | 
			
		||||
	m.Use(sessions.Sessions("my_session", s))
 | 
			
		||||
	m.Use(Google(&Options{
 | 
			
		||||
	// no need to configure
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	m.Get("/", func(s sessions.Session) {
 | 
			
		||||
		s.Set(keyToken, "dummy token")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	m.Get("/get", func(s sessions.Session) {
 | 
			
		||||
		if s.Get(keyToken) != nil {
 | 
			
		||||
			t.Errorf("User credentials are still kept in the session.")
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	logout, _ := http.NewRequest("GET", "/logout", nil)
 | 
			
		||||
	index, _ := http.NewRequest("GET", "/", nil)
 | 
			
		||||
 | 
			
		||||
	m.ServeHTTP(httptest.NewRecorder(), index)
 | 
			
		||||
	m.ServeHTTP(recorder, logout)
 | 
			
		||||
 | 
			
		||||
	if recorder.Code != 302 {
 | 
			
		||||
		t.Errorf("Not being redirected to the next page.")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_LogoutOnAccessTokenExpiration(t *testing.T) {
 | 
			
		||||
	recorder := httptest.NewRecorder()
 | 
			
		||||
	s := sessions.NewCookieStore([]byte("secret123"))
 | 
			
		||||
 | 
			
		||||
	m := martini.Classic()
 | 
			
		||||
	m.Use(sessions.Sessions("my_session", s))
 | 
			
		||||
	m.Use(Google(&Options{
 | 
			
		||||
	// no need to configure
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	m.Get("/addtoken", func(s sessions.Session) {
 | 
			
		||||
		s.Set(keyToken, "dummy token")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	m.Get("/", func(s sessions.Session) {
 | 
			
		||||
		if s.Get(keyToken) != nil {
 | 
			
		||||
			t.Errorf("User not logged out although access token is expired.")
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	addtoken, _ := http.NewRequest("GET", "/addtoken", nil)
 | 
			
		||||
	index, _ := http.NewRequest("GET", "/", nil)
 | 
			
		||||
	m.ServeHTTP(recorder, addtoken)
 | 
			
		||||
	m.ServeHTTP(recorder, index)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_InjectedTokens(t *testing.T) {
 | 
			
		||||
	recorder := httptest.NewRecorder()
 | 
			
		||||
	m := martini.Classic()
 | 
			
		||||
	m.Use(sessions.Sessions("my_session", sessions.NewCookieStore([]byte("secret123"))))
 | 
			
		||||
	m.Use(Google(&Options{
 | 
			
		||||
	// no need to configure
 | 
			
		||||
	}))
 | 
			
		||||
	m.Get("/", func(tokens Tokens) string {
 | 
			
		||||
		return "Hello world!"
 | 
			
		||||
	})
 | 
			
		||||
	r, _ := http.NewRequest("GET", "/", nil)
 | 
			
		||||
	m.ServeHTTP(recorder, r)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_LoginRequired(t *testing.T) {
 | 
			
		||||
	recorder := httptest.NewRecorder()
 | 
			
		||||
	m := martini.Classic()
 | 
			
		||||
	m.Use(sessions.Sessions("my_session", sessions.NewCookieStore([]byte("secret123"))))
 | 
			
		||||
	m.Use(Google(&Options{
 | 
			
		||||
	// no need to configure
 | 
			
		||||
	}))
 | 
			
		||||
	m.Get("/", LoginRequired, func(tokens Tokens) string {
 | 
			
		||||
		return "Hello world!"
 | 
			
		||||
	})
 | 
			
		||||
	r, _ := http.NewRequest("GET", "/", nil)
 | 
			
		||||
	m.ServeHTTP(recorder, r)
 | 
			
		||||
	if recorder.Code != 302 {
 | 
			
		||||
		t.Errorf("Not being redirected to the auth page although user is not logged in.")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,25 +0,0 @@
 | 
			
		||||
{{template "mail/base.html" .}}
 | 
			
		||||
{{define "title"}}
 | 
			
		||||
	{{if eq .Lang "zh-CN"}}
 | 
			
		||||
		 {{.User.NickName}},重置账户密码
 | 
			
		||||
	{{end}}
 | 
			
		||||
	{{if eq .Lang "en-US"}}
 | 
			
		||||
		{{.User.NickName}}, reset your password
 | 
			
		||||
	{{end}}
 | 
			
		||||
{{end}}
 | 
			
		||||
{{define "body"}}
 | 
			
		||||
	{{if eq .Lang "zh-CN"}}
 | 
			
		||||
		<p style="margin:0;padding:0 0 9px 0;">点击链接重置密码,{{.ResetPwdCodeLives}} 分钟内有效</p>
 | 
			
		||||
		<p style="margin:0;padding:0 0 9px 0;">
 | 
			
		||||
			<a href="{{.AppUrl}}reset/{{.Code}}">{{.AppUrl}}reset/{{.Code}}</a>
 | 
			
		||||
		</p>
 | 
			
		||||
		<p style="margin:0;padding:0 0 9px 0;">如果链接点击无反应,请复制到浏览器打开。</p>
 | 
			
		||||
	{{end}}
 | 
			
		||||
	{{if eq .Lang "en-US"}}
 | 
			
		||||
		<p style="margin:0;padding:0 0 9px 0;">Please click following link to reset your password in {{.ResetPwdCodeLives}} hours</p>
 | 
			
		||||
		<p style="margin:0;padding:0 0 9px 0;">
 | 
			
		||||
			<a href="{{.AppUrl}}reset/{{.Code}}">{{.AppUrl}}reset/{{.Code}}</a>
 | 
			
		||||
		</p>
 | 
			
		||||
		<p style="margin:0;padding:0 0 9px 0;">Copy and paste it to your browser if it's not working.</p>
 | 
			
		||||
	{{end}}
 | 
			
		||||
{{end}}
 | 
			
		||||
@ -1,36 +0,0 @@
 | 
			
		||||
{{template "base/head" .}}
 | 
			
		||||
{{template "base/navbar" .}}
 | 
			
		||||
<div id="body" class="container">
 | 
			
		||||
    <form action="/user/activate" method="post" class="form-horizontal card" id="login-card">
 | 
			
		||||
        {{.CsrfTokenHtml}}
 | 
			
		||||
        <h3>Activate Your Account</h3>
 | 
			
		||||
        {{if .IsActivatePage}}
 | 
			
		||||
            {{if .ServiceNotEnabled}}
 | 
			
		||||
            <p>Sorry, Register Mail Confirmation has been disabled.</p>
 | 
			
		||||
            {{else if .ResendLimited}}
 | 
			
		||||
            <p>Sorry, you are sending activation e-mail too frequently, please wait 3 minutes.</p>
 | 
			
		||||
            {{else}}
 | 
			
		||||
            <p>New confirmation e-mail has been sent to <b>{{.SignedUser.Email}}</b>, please check your inbox within {{.Hours}} hours to complete your registeration.</p>
 | 
			
		||||
            <hr/>
 | 
			
		||||
            <a href="http://{{Mail2Domain .SignedUser.Email}}" class="btn btn-lg btn-success">Sign in to your e-mail</a>
 | 
			
		||||
            {{end}}
 | 
			
		||||
        {{else}}
 | 
			
		||||
            {{if .IsSendRegisterMail}}
 | 
			
		||||
            <p>A confirmation e-mail has been sent to <b>{{.Email}}</b>, please check your inbox within {{.Hours}} hours to complete your registeration.</p>
 | 
			
		||||
            <hr/>
 | 
			
		||||
            <a href="http://{{Mail2Domain .Email}}" class="btn btn-lg btn-success">Sign in to your e-mail</a>
 | 
			
		||||
            {{else if .IsActivateFailed}}
 | 
			
		||||
            <p>Sorry, your confirmation code has been exipired or not valid.</p>
 | 
			
		||||
            {{else}}
 | 
			
		||||
    		<p>Hi, {{.SignedUser.Name}}, you have an unconfirmed email address(<b>{{.SignedUser.Email}}</b>). If you haven't received a confirmation e-mail or need to resend a new one, please click botton below.</p>
 | 
			
		||||
    		<hr/>
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
                <div class="col-md-offset-4 col-md-6">
 | 
			
		||||
                    <button type="submit" class="btn btn-lg btn-primary">Click here to resend your active e-mail</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {{end}}
 | 
			
		||||
        {{end}}
 | 
			
		||||
    </form>
 | 
			
		||||
</div>
 | 
			
		||||
{{template "base/footer" .}}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user