<commit_before>package wikifier

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	httpdate "github.com/Songmu/go-httpdate"
	"github.com/cooper/quiki/markdown"
	strip "github.com/grokify/html-strip-tags-go"
)

// Page represents a single page or article, generally associated with a .page file.
// It provides the most basic public interface to parsing with the wikifier engine.
type Page struct {
	Source     string   // source content
	FilePath   string   // Path to the .page file
	VarsOnly   bool     // True if Parse() should only extract variables
	Opt        *PageOpt // page options
	styles     []styleEntry
	parser     *parser            // wikifier parser instance
	main       block              // main block
	Images     map[string][][]int // references to images
	Models     map[string]bool    // references to models
	PageLinks  map[string][]int   // references to other pages
	sectionN   int
	name       string
	headingIDs map[string]int
	Wiki       interface{} // only available during Parse() and HTML()
	markdown   bool        // true if FilePath points to a markdown source
	*variableScope
}

// PageInfo represents metadata associated with a page.
type PageInfo struct {
	Path      string     `json:"-"`                   // absolute filepath
	File      string     `json:"file,omitempty"`      // name with extension, always with forward slashes
	FileNE    string     `json:"file_ne,omitempty"`   // name without extension, always with forward slashes
	Created   *time.Time `json:"created,omitempty"`   // creation time
	Modified  *time.Time `json:"modified,omitempty"`  // modify time
	Draft     bool       `json:"draft,omitempty"`     // true if page is marked as draft
	Generated bool       `json:"generated,omitempty"` // true if page was generated from another source
	Redirect  string     `json:"redirect,omitempty"`  // path page is to redirect to
	FmtTitle  HTML       `json:"fmt_title,omitempty"` // title with formatting tags
	Title     string     `json:"title,omitempty"`     // title without tags
	Author    string     `json:"author,omitempty"`    // author's name
}

// NewPage creates a page given its filepath.
func NewPage(filePath string) *Page {
	myOpt := defaultPageOpt // copy
	return &Page{
		FilePath:      filePath,
		Opt:           &myOpt,
		variableScope: newVariableScope(),
		Images:        make(map[string][][]int),
		Models:        make(map[string]bool),
		PageLinks:     make(map[string][]int),
		headingIDs:    make(map[string]int),
		markdown:      strings.HasSuffix(filePath, ".md"),
	}
}

// NewPageSource creates a page given some source code.
func NewPageSource(source string) *Page {
	p := NewPage("")
	p.Source = source
	return p
}

// NewPagePath creates a page given its filepath and relative name.
func NewPagePath(filePath, name string) *Page {
	p := NewPage(filePath)
	p.name = name
	return p
}

// Parse opens the page file and attempts to parse it, returning any errors encountered.
func (p *Page) Parse() error {
	p.parser = newParser()
	p.main = p.parser.block
	defer p.resetParseState()

	// create reader from file path or source code provided
	var reader io.Reader
	if p.Source != "" {
		reader = strings.NewReader(p.Source)
	} else if p.markdown && p.FilePath != "" {
		md, err := ioutil.ReadFile(p.FilePath)
		if err != nil {
			return err
		}
		d := markdown.Run(md)
		fmt.Println(string(d))
		reader = bytes.NewReader(d)
	} else if p.FilePath != "" {
		file, err := os.Open(p.FilePath)
		if err != nil {
			return err
		}
		defer file.Close()
		reader = file
	} else {
		return errors.New("neither Source nor FilePath provided")
	}

	// parse line-by-line
	scanner := bufio.NewScanner(reader)
	for scanner.Scan() {
		if err := p.parser.parseLine(scanner.Bytes(), p); err != nil {
			return err
		}
	}
	if err := scanner.Err(); err != nil {
		return err
	}

	// TODO: check if p.parser.catch != main block
	if p.parser.catch != p.main {
		if p.parser.catch == p.parser.block {
			return fmt.Errorf("%s{} not closed; started at %d", p.parser.block.blockType(), p.parser.block.openPosition())
		}
		return errors.New(string(p.parser.catch.catchType()) + " not closed")
	}

	// parse the blocks, unless we only want vars
	if !p.VarsOnly {
		p.main.parse(p)
	}

	// inject variables set in the page to page opts
	if err := InjectPageOpt(p, p.Opt); err != nil {
		// TODO: position
		return err
	}

	return nil
}

// HTML generates and returns the HTML code for the page.
// The page must be parsed with Parse before attempting this method.
func (p *Page) HTML() HTML {
	// TODO: cache and then recursively destroy elements
	return generateBlock(p.main, p)
}

// Exists is true if the page exists.
func (p *Page) Exists() bool {
	if p.Source != "" {
		return true
	}
	_, err := os.Stat(p.FilePath)
	return err == nil
}

// CacheExists is true if the page cache file exists.
func (p *Page) CacheExists() bool {
	_, err := os.Stat(p.CachePath())
	return err == nil
}

// Name returns the resolved page name with extension.
//
// This DOES take symbolic links into account.
// and DOES include the page prefix if applicable.
// Any prefix will have forward slashes regardless of OS.
//
func (p *Page) Name() string {
	dir := pageAbs(p.Opt.Dir.Page)
	path := p.Path()

	// make relative to page directory
	if name, err := filepath.Rel(dir, path); err == nil {

		// if it refers to an outside dir, forget this
		if strings.Index(name, "..") != -1 {
			return p.RelName()
		}

		// remove possible leading slash
		name = strings.TrimPrefix(filepath.ToSlash(name), "/") // path/to/quiki/doc/language.md
		return name
	}

	// if the path cannot be made relative to the page dir,
	// it is probably a symlink to something external
	return p.RelName()
}

// OSName is like Name, except it uses the native path separator.
// It should be used for file operations only.
func (p *Page) OSName() string {
	return filepath.FromSlash(p.Name())
}

// NameNE returns the resolved page name with No Extension.
func (p *Page) NameNE() string {
	return PageNameNE(p.Name())
}

// OSNameNE is like NameNE, except it uses the native path separator.
// It should be used for file operations only.
func (p *Page) OSNameNE() string {
	return filepath.FromSlash(p.NameNE())
}

// Prefix returns the page prefix.
//
// For example, for a page named a/b.page, this is a.
// For a page named a.page, this is an empty string.
//
func (p *Page) Prefix() string {
	dir := strings.TrimSuffix(filepath.ToSlash(filepath.Dir(p.Name())), "/")
	if dir == "." {
		return ""
	}
	return dir
}

// Path returns the absolute path to the page as resolved.
// If the path does not resolve, returns an empty string.
func (p *Page) Path() string {
	return pageAbs(p.RelPath())
}

// RelName returns the unresolved page filename, with or without extension.
// This does NOT take symbolic links into account.
// It is not guaranteed to exist.
func (p *Page) RelName() string {

	// name is predetermined
	if p.name != "" {
		return p.name
	}

	dir := pageAbs(p.Opt.Dir.Page)
	path := p.RelPath() // this is what makes it different from Name()

	// make relative to page directory
	if name, err := filepath.Rel(dir, path); err == nil {

		// remove possible leading slash
		name = strings.TrimPrefix(filepath.ToSlash(name), "/") // path/to/quiki/doc/language.md
		return name
	}

	// if the path cannot be made relative to the page dir,
	// it is probably a symlink to something external
	return filepath.ToSlash(path)
}

// RelNameNE returns the unresolved page name with No Extension, relative to
// the page directory option.
// This does NOT take symbolic links into account.
// It is not guaranteed to exist.
func (p *Page) RelNameNE() string {
	return PageNameNE(p.RelName())
}

// RelPath returns the unresolved file path to the page.
// It may be a relative or absolute path.
// It is not guaranteed to exist.
func (p *Page) RelPath() string {
	if p.FilePath != "" {
		return p.FilePath
	}
	return filepath.Join(p.Opt.Dir.Page, p.name)
}

// Redirect returns the location to which the page redirects, if any.
// This may be a relative or absolute URL, suitable for use in a Location header.
func (p *Page) Redirect() string {

	// symbolic link redirect
	if p.IsSymlink() {
		return pageAbs(filepath.Join(p.Opt.Root.Page, p.NameNE()))
	}

	// @page.redirect
	if link, err := p.GetStr("page.redirect"); err != nil {
		// FIXME: is there anyway to produce a warning for wrong variable type?
	} else if ok, target, _, _, _ := p.parseLink(link); ok {
		return target
	}

	return ""
}

// IsSymlink returns true if the page is a symbolic link to another file within
// the page directory. If it is symlinked to somewhere outside the page directory,
// it is treated as a normal page rather than a redirect.
func (p *Page) IsSymlink() bool {
	dirPage := pageAbs(p.Opt.Dir.Page)
	if !strings.HasPrefix(p.Path(), dirPage) {
		return false
	}
	fi, _ := os.Lstat(p.RelPath())
	return fi.Mode()&os.ModeSymlink != 0
}

// Created returns the page creation time.
func (p *Page) Created() time.Time {
	var t time.Time
	// FIXME: maybe produce a warning if this is not in the right format
	created, _ := p.GetStr("page.created")
	if created == "" {
		return t
	}
	if unix, err := strconv.ParseInt(created, 10, 0); err == nil {
		return time.Unix(unix, 0)
	}
	t, _ = httpdate.Str2Time(created, time.UTC)
	return t
}

// Modified returns the page modification time.
func (p *Page) Modified() time.Time {
	fi, _ := os.Lstat(p.Path())
	return fi.ModTime()
}

// CachePath returns the absolute path to the page cache file.
func (p *Page) CachePath() string {
	osName := p.OSName() + ".cache" // os-specific cache name
	MakeDir(filepath.Join(p.Opt.Dir.Cache, "page"), osName)
	return pageAbs(filepath.Join(p.Opt.Dir.Cache, "page", osName))
}

// CacheModified returns the page cache file time.
func (p *Page) CacheModified() time.Time {
	fi, _ := os.Lstat(p.CachePath())
	return fi.ModTime()
}

// SearchPath returns the absolute path to the page search text file.
func (p *Page) SearchPath() string {
	osName := p.OSName() + ".txt" // os-specific text file name
	MakeDir(filepath.Join(p.Opt.Dir.Cache, "page"), osName)
	return pageAbs(filepath.Join(p.Opt.Dir.Cache, "page", osName))
}

// Draft returns true if the page is marked as a draft.
func (p *Page) Draft() bool {
	b, _ := p.GetBool("page.draft")
	return b
}

// Generated returns true if the page was auto-generated
// from some other source content.
func (p *Page) Generated() bool {
	b, _ := p.GetBool("page.generated")
	return b
}

// Author returns the page author's name, if any.
func (p *Page) Author() string {
	s, _ := p.GetStr("page.author")
	return s
}

// FmtTitle returns the page title, preserving any possible text formatting.
func (p *Page) FmtTitle() HTML {
	s, _ := p.GetStr("page.title")
	return HTML(s)
}

// Title returns the page title with HTML text formatting tags stripped.
func (p *Page) Title() string {
	return strip.StripTags(string(p.FmtTitle()))
}

// TitleOrName returns the result of Title if available, otherwise that of Name.
func (p *Page) TitleOrName() string {
	if title := p.Title(); title != "" {
		return title
	}
	return p.Name()
}

// Categories returns a list of categories the page belongs to.
func (p *Page) Categories() []string {
	obj, err := p.GetObj("category")
	if err != nil {
		return nil
	}
	catMap, ok := obj.(*Map)
	if !ok {
		return nil
	}
	return catMap.Keys()
}

// Info returns the PageInfo for the page.
func (p *Page) Info() PageInfo {
	info := PageInfo{
		File:      p.Name(),
		FileNE:    p.NameNE(),
		Draft:     p.Draft(),
		Generated: p.Generated(),
		Redirect:  p.Redirect(),
		FmtTitle:  p.FmtTitle(),
		Title:     p.Title(),
		Author:    p.Author(),
	}
	mod, create := p.Modified(), p.Created()
	if !mod.IsZero() {
		info.Modified = &mod
		info.Created = &mod // fallback
	}
	if !create.IsZero() {
		info.Created = &create
	}
	return info
}

func (p *Page) mainBlock() block {
	return p.main
}

// resets the parser
func (p *Page) resetParseState() {
	// TODO: recursively destroy blocks
	p.parser = nil
}

func pageAbs(path string) string {
	if abs, _ := filepath.Abs(path); abs != "" {
		path = abs
	}
	if followed, _ := filepath.EvalSymlinks(path); followed != "" {
		return followed
	}
	return path
}
<commit_msg>add page.External() to determine whether resolved page path is inside page dir<commit_after>package wikifier

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	httpdate "github.com/Songmu/go-httpdate"
	"github.com/cooper/quiki/markdown"
	strip "github.com/grokify/html-strip-tags-go"
)

// Page represents a single page or article, generally associated with a .page file.
// It provides the most basic public interface to parsing with the wikifier engine.
type Page struct {
	Source     string   // source content
	FilePath   string   // Path to the .page file
	VarsOnly   bool     // True if Parse() should only extract variables
	Opt        *PageOpt // page options
	styles     []styleEntry
	parser     *parser            // wikifier parser instance
	main       block              // main block
	Images     map[string][][]int // references to images
	Models     map[string]bool    // references to models
	PageLinks  map[string][]int   // references to other pages
	sectionN   int
	name       string
	headingIDs map[string]int
	Wiki       interface{} // only available during Parse() and HTML()
	markdown   bool        // true if FilePath points to a markdown source
	*variableScope
}

// PageInfo represents metadata associated with a page.
type PageInfo struct {
	Path      string     `json:"-"`                   // absolute filepath
	File      string     `json:"file,omitempty"`      // name with extension, always with forward slashes
	FileNE    string     `json:"file_ne,omitempty"`   // name without extension, always with forward slashes
	Created   *time.Time `json:"created,omitempty"`   // creation time
	Modified  *time.Time `json:"modified,omitempty"`  // modify time
	Draft     bool       `json:"draft,omitempty"`     // true if page is marked as draft
	Generated bool       `json:"generated,omitempty"` // true if page was generated from another source
	External  bool       `json:"external,omitempty"`  // true if page is outside the page directory
	Redirect  string     `json:"redirect,omitempty"`  // path page is to redirect to
	FmtTitle  HTML       `json:"fmt_title,omitempty"` // title with formatting tags
	Title     string     `json:"title,omitempty"`     // title without tags
	Author    string     `json:"author,omitempty"`    // author's name
}

// NewPage creates a page given its filepath.
func NewPage(filePath string) *Page {
	myOpt := defaultPageOpt // copy
	return &Page{
		FilePath:      filePath,
		Opt:           &myOpt,
		variableScope: newVariableScope(),
		Images:        make(map[string][][]int),
		Models:        make(map[string]bool),
		PageLinks:     make(map[string][]int),
		headingIDs:    make(map[string]int),
		markdown:      strings.HasSuffix(filePath, ".md"),
	}
}

// NewPageSource creates a page given some source code.
func NewPageSource(source string) *Page {
	p := NewPage("")
	p.Source = source
	return p
}

// NewPagePath creates a page given its filepath and relative name.
func NewPagePath(filePath, name string) *Page {
	p := NewPage(filePath)
	p.name = name
	return p
}

// Parse opens the page file and attempts to parse it, returning any errors encountered.
func (p *Page) Parse() error {
	p.parser = newParser()
	p.main = p.parser.block
	defer p.resetParseState()

	// create reader from file path or source code provided
	var reader io.Reader
	if p.Source != "" {
		reader = strings.NewReader(p.Source)
	} else if p.markdown && p.FilePath != "" {
		md, err := ioutil.ReadFile(p.FilePath)
		if err != nil {
			return err
		}
		d := markdown.Run(md)
		fmt.Println(string(d))
		reader = bytes.NewReader(d)
	} else if p.FilePath != "" {
		file, err := os.Open(p.FilePath)
		if err != nil {
			return err
		}
		defer file.Close()
		reader = file
	} else {
		return errors.New("neither Source nor FilePath provided")
	}

	// parse line-by-line
	scanner := bufio.NewScanner(reader)
	for scanner.Scan() {
		if err := p.parser.parseLine(scanner.Bytes(), p); err != nil {
			return err
		}
	}
	if err := scanner.Err(); err != nil {
		return err
	}

	// TODO: check if p.parser.catch != main block
	if p.parser.catch != p.main {
		if p.parser.catch == p.parser.block {
			return fmt.Errorf("%s{} not closed; started at %d", p.parser.block.blockType(), p.parser.block.openPosition())
		}
		return errors.New(string(p.parser.catch.catchType()) + " not closed")
	}

	// parse the blocks, unless we only want vars
	if !p.VarsOnly {
		p.main.parse(p)
	}

	// inject variables set in the page to page opts
	if err := InjectPageOpt(p, p.Opt); err != nil {
		// TODO: position
		return err
	}

	return nil
}

// HTML generates and returns the HTML code for the page.
// The page must be parsed with Parse before attempting this method.
func (p *Page) HTML() HTML {
	// TODO: cache and then recursively destroy elements
	return generateBlock(p.main, p)
}

// Exists is true if the page exists.
func (p *Page) Exists() bool {
	if p.Source != "" {
		return true
	}
	_, err := os.Stat(p.FilePath)
	return err == nil
}

// CacheExists is true if the page cache file exists.
func (p *Page) CacheExists() bool {
	_, err := os.Stat(p.CachePath())
	return err == nil
}

// Name returns the resolved page name with extension.
//
// This DOES take symbolic links into account.
// and DOES include the page prefix if applicable.
// Any prefix will have forward slashes regardless of OS.
//
func (p *Page) Name() string {
	dir := pageAbs(p.Opt.Dir.Page)
	path := p.Path()

	// make relative to page directory
	if name, err := filepath.Rel(dir, path); err == nil {

		// if it refers to an outside dir, forget this
		if strings.Index(name, "..") != -1 {
			return p.RelName()
		}

		// remove possible leading slash
		name = strings.TrimPrefix(filepath.ToSlash(name), "/") // path/to/quiki/doc/language.md
		return name
	}

	// if the path cannot be made relative to the page dir,
	// it is probably a symlink to something external
	return p.RelName()
}

// OSName is like Name, except it uses the native path separator.
// It should be used for file operations only.
func (p *Page) OSName() string {
	return filepath.FromSlash(p.Name())
}

// NameNE returns the resolved page name with No Extension.
func (p *Page) NameNE() string {
	return PageNameNE(p.Name())
}

// OSNameNE is like NameNE, except it uses the native path separator.
// It should be used for file operations only.
func (p *Page) OSNameNE() string {
	return filepath.FromSlash(p.NameNE())
}

// Prefix returns the page prefix.
//
// For example, for a page named a/b.page, this is a.
// For a page named a.page, this is an empty string.
//
func (p *Page) Prefix() string {
	dir := strings.TrimSuffix(filepath.ToSlash(filepath.Dir(p.Name())), "/")
	if dir == "." {
		return ""
	}
	return dir
}

// Path returns the absolute path to the page as resolved.
// If the path does not resolve, returns an empty string.
func (p *Page) Path() string {
	return pageAbs(p.RelPath())
}

// RelName returns the unresolved page filename, with or without extension.
// This does NOT take symbolic links into account.
// It is not guaranteed to exist.
func (p *Page) RelName() string {

	// name is predetermined
	if p.name != "" {
		return p.name
	}

	dir := pageAbs(p.Opt.Dir.Page)
	path := p.RelPath() // this is what makes it different from Name()

	// make relative to page directory
	if name, err := filepath.Rel(dir, path); err == nil {

		// remove possible leading slash
		name = strings.TrimPrefix(filepath.ToSlash(name), "/") // path/to/quiki/doc/language.md
		return name
	}

	// if the path cannot be made relative to the page dir,
	// it is probably a symlink to something external
	return filepath.ToSlash(path)
}

// RelNameNE returns the unresolved page name with No Extension, relative to
// the page directory option.
// This does NOT take symbolic links into account.
// It is not guaranteed to exist.
func (p *Page) RelNameNE() string {
	return PageNameNE(p.RelName())
}

// RelPath returns the unresolved file path to the page.
// It may be a relative or absolute path.
// It is not guaranteed to exist.
func (p *Page) RelPath() string {
	if p.FilePath != "" {
		return p.FilePath
	}
	return filepath.Join(p.Opt.Dir.Page, p.name)
}

// Redirect returns the location to which the page redirects, if any.
// This may be a relative or absolute URL, suitable for use in a Location header.
func (p *Page) Redirect() string {

	// symbolic link redirect
	if p.IsSymlink() {
		return pageAbs(filepath.Join(p.Opt.Root.Page, p.NameNE()))
	}

	// @page.redirect
	if link, err := p.GetStr("page.redirect"); err != nil {
		// FIXME: is there anyway to produce a warning for wrong variable type?
	} else if ok, target, _, _, _ := p.parseLink(link); ok {
		return target
	}

	return ""
}

// IsSymlink returns true if the page is a symbolic link to another file within
// the page directory. If it is symlinked to somewhere outside the page directory,
// it is treated as a normal page rather than a redirect.
func (p *Page) IsSymlink() bool {
	dirPage := pageAbs(p.Opt.Dir.Page)
	if !strings.HasPrefix(p.Path(), dirPage) {
		return false
	}
	fi, _ := os.Lstat(p.RelPath())
	return fi.Mode()&os.ModeSymlink != 0
}

// Created returns the page creation time.
func (p *Page) Created() time.Time {
	var t time.Time
	// FIXME: maybe produce a warning if this is not in the right format
	created, _ := p.GetStr("page.created")
	if created == "" {
		return t
	}
	if unix, err := strconv.ParseInt(created, 10, 0); err == nil {
		return time.Unix(unix, 0)
	}
	t, _ = httpdate.Str2Time(created, time.UTC)
	return t
}

// Modified returns the page modification time.
func (p *Page) Modified() time.Time {
	fi, _ := os.Lstat(p.Path())
	return fi.ModTime()
}

// CachePath returns the absolute path to the page cache file.
func (p *Page) CachePath() string {
	osName := p.OSName() + ".cache" // os-specific cache name
	MakeDir(filepath.Join(p.Opt.Dir.Cache, "page"), osName)
	return pageAbs(filepath.Join(p.Opt.Dir.Cache, "page", osName))
}

// CacheModified returns the page cache file time.
func (p *Page) CacheModified() time.Time {
	fi, _ := os.Lstat(p.CachePath())
	return fi.ModTime()
}

// SearchPath returns the absolute path to the page search text file.
func (p *Page) SearchPath() string {
	osName := p.OSName() + ".txt" // os-specific text file name
	MakeDir(filepath.Join(p.Opt.Dir.Cache, "page"), osName)
	return pageAbs(filepath.Join(p.Opt.Dir.Cache, "page", osName))
}

// Draft returns true if the page is marked as a draft.
func (p *Page) Draft() bool {
	b, _ := p.GetBool("page.draft")
	return b
}

// Generated returns true if the page was auto-generated
// from some other source content.
func (p *Page) Generated() bool {
	b, _ := p.GetBool("page.generated")
	return b
}

// External returns true if the page is outside the page directory
// as defined by the configuration, with symlinks considered.
//
// If `dir.wiki` isn't set, External is always true
// (since the page is not associated with a wiki at all).
func (p *Page) External() bool {

	// not part of a wiki at all
	dirPage := pageAbs(p.Opt.Dir.Page)
	if dirPage == "" {
		return true
	}

	// cannot be made relative
	rel, err := filepath.Rel(dirPage, p.Path())
	if err != nil {
		return true
	}

	// contains ../ so it's not relative
	if strings.Contains(rel, ".."+string(os.PathSeparator)) {
		return true
	}
	if strings.Contains(rel, string(os.PathSeparator)+"..") {
		return true
	}

	// otherwise it's in there
	return false
}

// Author returns the page author's name, if any.
func (p *Page) Author() string {
	s, _ := p.GetStr("page.author")
	return s
}

// FmtTitle returns the page title, preserving any possible text formatting.
func (p *Page) FmtTitle() HTML {
	s, _ := p.GetStr("page.title")
	return HTML(s)
}

// Title returns the page title with HTML text formatting tags stripped.
func (p *Page) Title() string {
	return strip.StripTags(string(p.FmtTitle()))
}

// TitleOrName returns the result of Title if available, otherwise that of Name.
func (p *Page) TitleOrName() string {
	if title := p.Title(); title != "" {
		return title
	}
	return p.Name()
}

// Categories returns a list of categories the page belongs to.
func (p *Page) Categories() []string {
	obj, err := p.GetObj("category")
	if err != nil {
		return nil
	}
	catMap, ok := obj.(*Map)
	if !ok {
		return nil
	}
	return catMap.Keys()
}

// Info returns the PageInfo for the page.
func (p *Page) Info() PageInfo {
	info := PageInfo{
		File:      p.Name(),
		FileNE:    p.NameNE(),
		Draft:     p.Draft(),
		Generated: p.Generated(),
		External:  p.External(),
		Redirect:  p.Redirect(),
		FmtTitle:  p.FmtTitle(),
		Title:     p.Title(),
		Author:    p.Author(),
	}
	mod, create := p.Modified(), p.Created()
	if !mod.IsZero() {
		info.Modified = &mod
		info.Created = &mod // fallback
	}
	if !create.IsZero() {
		info.Created = &create
	}
	return info
}

func (p *Page) mainBlock() block {
	return p.main
}

// resets the parser
func (p *Page) resetParseState() {
	// TODO: recursively destroy blocks
	p.parser = nil
}

func pageAbs(path string) string {
	if abs, _ := filepath.Abs(path); abs != "" {
		path = abs
	}
	if followed, _ := filepath.EvalSymlinks(path); followed != "" {
		return followed
	}
	return path
}
<|endoftext|>


<commit_before>package wikifier

import (
	"bufio"
	"errors"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	httpdate "github.com/Songmu/go-httpdate"
)

// Page represents a single page or article, generally associated with a .page file.
// It provides the most basic public interface to parsing with the wikifier engine.
type Page struct {
	Source   string  // source content
	FilePath string  // Path to the .page file
	VarsOnly bool    // True if Parse() should only extract variables
	Opt      PageOpt // page options
	styles   []styleEntry
	parser   *parser // wikifier parser instance
	main     block   // main block
	images   map[string][][]int
	*variableScope
}

// NewPage creates a page given its filepath.
func NewPage(filePath string) *Page {
	return &Page{FilePath: filePath, Opt: defaultPageOpt, variableScope: newVariableScope()}
}

// NewPageSource creates a page given some source code.
func NewPageSource(source string) *Page {
	return &Page{Source: source, Opt: defaultPageOpt, variableScope: newVariableScope()}
}

// Parse opens the page file and attempts to parse it, returning any errors encountered.
func (p *Page) Parse() error {
	p.parser = newParser()
	p.main = p.parser.block

	// create reader from file path or source code provided
	var reader io.Reader
	if p.Source != "" {
		reader = strings.NewReader(p.Source)
	} else if p.FilePath != "" {
		file, err := os.Open(p.FilePath)
		if err != nil {
			return err
		}
		defer file.Close()
		reader = file
	} else {
		return errors.New("neither Source nor FilePath provided")
	}

	// parse line-by-line
	scanner := bufio.NewScanner(reader)
	for scanner.Scan() {
		if err := p.parser.parseLine(scanner.Bytes(), p); err != nil {
			return err
		}
	}
	if err := scanner.Err(); err != nil {
		return err
	}

	// TODO: check if p.parser.catch != main block

	// parse the blocks, unless we only want vars
	if !p.VarsOnly {
		p.main.parse(p)
	}

	// inject variables set in the page to page opts
	if err := InjectPageOpt(p, &p.Opt); err != nil {
		// TODO: position
		return err
	}

	return nil
}

// HTML generates and returns the HTML code for the page.
// The page must be parsed with Parse before attempting this method.
func (p *Page) HTML() HTML {
	// TODO: cache and then recursively destroy elements
	return generateBlock(p.main, p)
}

// Exists is true if the page exists.
func (p *Page) Exists() bool {
	if p.Source != "" {
		return true
	}
	_, err := os.Stat(p.FilePath)
	return err == nil
}

// Name returns the resolved page name, with or without extension.
//
// This does NOT take symbolic links into account.
// It DOES include the page prefix, however, if applicable.
//
func (p *Page) Name() string {
	dir := p.Opt.Dir.Page
	path := p.Path()
	name := strings.TrimPrefix(path, dir)
	name = strings.TrimPrefix(name, "/")
	if strings.Index(path, dir) != -1 {
		return filepath.Base(p.RelPath())
	}
	return name
}

// NameNE returns the resolved page name with No Extension.
func (p *Page) NameNE() string {
	return PageNameNE(p.Name())
}

// Prefix returns the page prefix.
//
// For example, for a page named a/b.page, this is a.
// For a page named a.page, this is an empty string.
//
func (p *Page) Prefix() string {
	dir := strings.TrimSuffix(filepath.Dir(p.Name()), "/")
	if dir == "." {
		return ""
	}
	return dir
}

// Path returns the absolute path to the page as resolved.
// If the path does not resolve, returns an empty string.
func (p *Page) Path() string {
	path, _ := filepath.Abs(p.RelPath())
	return path
}

// RelName returns the unresolved page filename, with or without extension.
// This does NOT take symbolic links into account.
// It is not guaranteed to exist.
func (p *Page) RelName() string {
	dir := p.Opt.Dir.Page
	path := p.RelPath()
	name := strings.TrimPrefix(path, dir)
	name = strings.TrimPrefix(name, "/")
	if strings.Index(path, dir) != -1 {
		return filepath.Base(p.RelPath())
	}
	return name
}

// RelNameNE returns the unresolved page name with No Extension, relative to
// the page directory option.
// This does NOT take symbolic links into account.
// It is not guaranteed to exist.
func (p *Page) RelNameNE() string {
	return PageNameNE(p.RelName())
}

// RelPath returns the unresolved file path to the page.
// It may be a relative or absolute path.
// It is not guaranteed to exist.
func (p *Page) RelPath() string {
	return p.FilePath
}

// Redirect returns the location to which the page redirects, if any.
// This may be a relative or absolute URL, suitable for use in a Location header.
func (p *Page) Redirect() string {

	// symbolic link redirect
	if p.IsSymlink() {
		return p.Opt.Root.Page + "/" + p.NameNE()
	}

	// @page.redirect
	if link, err := p.GetStr("page.redirect"); err != nil {
		// FIXME: is there anyway to produce a warning for wrong variable type?
	} else if ok, _, target, _, _, _ := parseLink(link); ok {
		return target
	}

	return ""
}

// IsSymlink returns true if the page is a symbolic link to another file within
// the page directory. If it is symlinked to somewhere outside the page directory,
// it is treated as a normal page rather than a redirect.
func (p *Page) IsSymlink() bool {
	if !strings.HasPrefix(p.Prefix(), p.Opt.Dir.Page) {
		return false
	}
	fi, _ := os.Lstat(p.RelPath())
	return fi.Mode()&os.ModeSymlink != 0
}

// Created returns the page creation time.
func (p *Page) Created() time.Time {
	var t time.Time
	// FIXME: maybe produce a warning if this is not in the right format
	created, _ := p.GetStr("page.created")
	if created == "" {
		return t
	}
	if unix, err := strconv.ParseInt(created, 10, 0); err != nil {
		return time.Unix(unix, 0)
	}
	t, _ = httpdate.Str2Time(created, time.UTC)
	return t
}

// Modified returns the page modification time.
func (p *Page) Modified() time.Time {
	fi, _ := os.Lstat(p.Path())
	return fi.ModTime()
}

// CachePath returns the path to the page cache file.
func (p *Page) CachePath() string {
	// FIXME: makedir
	return p.Opt.Dir.Cache + "/page/" + page.Name() + ".cache"
}

// CacheModified returns the page cache file time.
func (p *Page) CacheModified() time.Time {
	fi, _ := os.Lstat(p.CachePath())
	return fi.ModTime()
}

// # absolute path to search text file
// sub search_path {
//     my $page = shift;
//     return abs_path($page->{search_path})
//         if length $page->{search_path};
//     make_dir($page->opt('dir.cache').'/page', $page->name);
//     return $page->{abs_search_path}
//         if length $page->{abs_search_path};
//     return $page->{cached_props}{search} //= abs_path(
//         $page->opt('dir.cache').'/page/'.$page->name.'.txt'
//     );
// }

// # page info to be used in results, stored in cats/cache files
// sub page_info {
//     my $page = shift;
//     return filter_nonempty {
//         mod_unix    => $page->modified,
//         created     => $page->created,
//         draft       => $page->draft,
//         generated   => $page->generated,
//         redirect    => $page->redirect,
//         fmt_title   => $page->fmt_title,
//         title       => $page->title,
//         author      => $page->author
//     };
// }

// sub _bool ($) { shift() ? \1 : undef }

// # page draft from @page.draft
// sub draft {
//     my $page = shift;
//     return _bool $page->get('page.draft');
// }

// # page generated from @page.generated
// sub generated {
//     my $page = shift;
//     return _bool $page->get('page.generated');
// }

// # page author from @page.author
// sub author {
//     my $page = shift;
//     return no_length_undef trim $page->get('page.author');
// }

// # formatted title from @page.title
// sub fmt_title {
//     my $page = shift;
//     return no_length_undef trim $page->get('page.title');
// }

// # tag-stripped version of page title
// sub title {
//     my $page = shift;
//     my $title = $page->fmt_title;
//     return length $title ? $stripper->parse($title) : undef;
// }

// # title if available; otherwise filename
// sub title_or_name {
//     my $page = shift;
//     return $page->title // $page->name;
// }

// resets the parser
func (p *Page) resetParseState() {
	// TODO: recursively destroy blocks
	p.parser = nil
}
<commit_msg>SearchPath<commit_after>package wikifier

import (
	"bufio"
	"errors"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	httpdate "github.com/Songmu/go-httpdate"
)

// Page represents a single page or article, generally associated with a .page file.
// It provides the most basic public interface to parsing with the wikifier engine.
type Page struct {
	Source   string  // source content
	FilePath string  // Path to the .page file
	VarsOnly bool    // True if Parse() should only extract variables
	Opt      PageOpt // page options
	styles   []styleEntry
	parser   *parser // wikifier parser instance
	main     block   // main block
	images   map[string][][]int
	*variableScope
}

// NewPage creates a page given its filepath.
func NewPage(filePath string) *Page {
	return &Page{FilePath: filePath, Opt: defaultPageOpt, variableScope: newVariableScope()}
}

// NewPageSource creates a page given some source code.
func NewPageSource(source string) *Page {
	return &Page{Source: source, Opt: defaultPageOpt, variableScope: newVariableScope()}
}

// Parse opens the page file and attempts to parse it, returning any errors encountered.
func (p *Page) Parse() error {
	p.parser = newParser()
	p.main = p.parser.block

	// create reader from file path or source code provided
	var reader io.Reader
	if p.Source != "" {
		reader = strings.NewReader(p.Source)
	} else if p.FilePath != "" {
		file, err := os.Open(p.FilePath)
		if err != nil {
			return err
		}
		defer file.Close()
		reader = file
	} else {
		return errors.New("neither Source nor FilePath provided")
	}

	// parse line-by-line
	scanner := bufio.NewScanner(reader)
	for scanner.Scan() {
		if err := p.parser.parseLine(scanner.Bytes(), p); err != nil {
			return err
		}
	}
	if err := scanner.Err(); err != nil {
		return err
	}

	// TODO: check if p.parser.catch != main block

	// parse the blocks, unless we only want vars
	if !p.VarsOnly {
		p.main.parse(p)
	}

	// inject variables set in the page to page opts
	if err := InjectPageOpt(p, &p.Opt); err != nil {
		// TODO: position
		return err
	}

	return nil
}

// HTML generates and returns the HTML code for the page.
// The page must be parsed with Parse before attempting this method.
func (p *Page) HTML() HTML {
	// TODO: cache and then recursively destroy elements
	return generateBlock(p.main, p)
}

// Exists is true if the page exists.
func (p *Page) Exists() bool {
	if p.Source != "" {
		return true
	}
	_, err := os.Stat(p.FilePath)
	return err == nil
}

// Name returns the resolved page name, with or without extension.
//
// This does NOT take symbolic links into account.
// It DOES include the page prefix, however, if applicable.
//
func (p *Page) Name() string {
	dir := p.Opt.Dir.Page
	path := p.Path()
	name := strings.TrimPrefix(path, dir)
	name = strings.TrimPrefix(name, "/")
	if strings.Index(path, dir) != -1 {
		return filepath.Base(p.RelPath())
	}
	return name
}

// NameNE returns the resolved page name with No Extension.
func (p *Page) NameNE() string {
	return PageNameNE(p.Name())
}

// Prefix returns the page prefix.
//
// For example, for a page named a/b.page, this is a.
// For a page named a.page, this is an empty string.
//
func (p *Page) Prefix() string {
	dir := strings.TrimSuffix(filepath.Dir(p.Name()), "/")
	if dir == "." {
		return ""
	}
	return dir
}

// Path returns the absolute path to the page as resolved.
// If the path does not resolve, returns an empty string.
func (p *Page) Path() string {
	path, _ := filepath.Abs(p.RelPath())
	return path
}

// RelName returns the unresolved page filename, with or without extension.
// This does NOT take symbolic links into account.
// It is not guaranteed to exist.
func (p *Page) RelName() string {
	dir := p.Opt.Dir.Page
	path := p.RelPath()
	name := strings.TrimPrefix(path, dir)
	name = strings.TrimPrefix(name, "/")
	if strings.Index(path, dir) != -1 {
		return filepath.Base(p.RelPath())
	}
	return name
}

// RelNameNE returns the unresolved page name with No Extension, relative to
// the page directory option.
// This does NOT take symbolic links into account.
// It is not guaranteed to exist.
func (p *Page) RelNameNE() string {
	return PageNameNE(p.RelName())
}

// RelPath returns the unresolved file path to the page.
// It may be a relative or absolute path.
// It is not guaranteed to exist.
func (p *Page) RelPath() string {
	return p.FilePath
}

// Redirect returns the location to which the page redirects, if any.
// This may be a relative or absolute URL, suitable for use in a Location header.
func (p *Page) Redirect() string {

	// symbolic link redirect
	if p.IsSymlink() {
		return p.Opt.Root.Page + "/" + p.NameNE()
	}

	// @page.redirect
	if link, err := p.GetStr("page.redirect"); err != nil {
		// FIXME: is there anyway to produce a warning for wrong variable type?
	} else if ok, _, target, _, _, _ := parseLink(link); ok {
		return target
	}

	return ""
}

// IsSymlink returns true if the page is a symbolic link to another file within
// the page directory. If it is symlinked to somewhere outside the page directory,
// it is treated as a normal page rather than a redirect.
func (p *Page) IsSymlink() bool {
	if !strings.HasPrefix(p.Prefix(), p.Opt.Dir.Page) {
		return false
	}
	fi, _ := os.Lstat(p.RelPath())
	return fi.Mode()&os.ModeSymlink != 0
}

// Created returns the page creation time.
func (p *Page) Created() time.Time {
	var t time.Time
	// FIXME: maybe produce a warning if this is not in the right format
	created, _ := p.GetStr("page.created")
	if created == "" {
		return t
	}
	if unix, err := strconv.ParseInt(created, 10, 0); err != nil {
		return time.Unix(unix, 0)
	}
	t, _ = httpdate.Str2Time(created, time.UTC)
	return t
}

// Modified returns the page modification time.
func (p *Page) Modified() time.Time {
	fi, _ := os.Lstat(p.Path())
	return fi.ModTime()
}

// CachePath returns the absolute path to the page cache file.
func (p *Page) CachePath() string {
	// FIXME: makedir
	path, _ := filepath.Abs(p.Opt.Dir.Cache + "/page/" + p.Name() + ".cache")
	return path
}

// CacheModified returns the page cache file time.
func (p *Page) CacheModified() time.Time {
	fi, _ := os.Lstat(p.CachePath())
	return fi.ModTime()
}

// # absolute path to search text file
// sub search_path {
//     my $page = shift;
//     return abs_path($page->{search_path})
//         if length $page->{search_path};
//     make_dir($page->opt('dir.cache').'/page', $page->name);
//     return $page->{abs_search_path}
//         if length $page->{abs_search_path};
//     return $page->{cached_props}{search} //= abs_path(
//         $page->opt('dir.cache').'/page/'.$page->name.'.txt'
//     );
// }

// SearchPath returns the path to the page search text file.
func (p *Page) SearchPath() string {
	// FIXME: makedir
	path, _ := filepath.Abs(p.Opt.Dir.Cache + "/page/" + p.Name() + ".txt")
	return path
}

// # page info to be used in results, stored in cats/cache files
// sub page_info {
//     my $page = shift;
//     return filter_nonempty {
//         mod_unix    => $page->modified,
//         created     => $page->created,
//         draft       => $page->draft,
//         generated   => $page->generated,
//         redirect    => $page->redirect,
//         fmt_title   => $page->fmt_title,
//         title       => $page->title,
//         author      => $page->author
//     };
// }

// sub _bool ($) { shift() ? \1 : undef }

// # page draft from @page.draft
// sub draft {
//     my $page = shift;
//     return _bool $page->get('page.draft');
// }

// # page generated from @page.generated
// sub generated {
//     my $page = shift;
//     return _bool $page->get('page.generated');
// }

// # page author from @page.author
// sub author {
//     my $page = shift;
//     return no_length_undef trim $page->get('page.author');
// }

// # formatted title from @page.title
// sub fmt_title {
//     my $page = shift;
//     return no_length_undef trim $page->get('page.title');
// }

// # tag-stripped version of page title
// sub title {
//     my $page = shift;
//     my $title = $page->fmt_title;
//     return length $title ? $stripper->parse($title) : undef;
// }

// # title if available; otherwise filename
// sub title_or_name {
//     my $page = shift;
//     return $page->title // $page->name;
// }

// resets the parser
func (p *Page) resetParseState() {
	// TODO: recursively destroy blocks
	p.parser = nil
}
<|endoftext|>


<commit_before>// Copyright 2015 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package cloudimagemetadata_test

import (
	"github.com/juju/errors"
	gitjujutesting "github.com/juju/testing"
	jc "github.com/juju/testing/checkers"
	jujutxn "github.com/juju/txn"
	txntesting "github.com/juju/txn/testing"
	gc "gopkg.in/check.v1"
	"gopkg.in/mgo.v2"

	"github.com/juju/juju/state/cloudimagemetadata"
	"github.com/juju/juju/testing"
)

type keyMetadataSuite struct {
	testing.BaseSuite
}

var _ = gc.Suite(&keyMetadataSuite{})

var keyTestData = []struct {
	about       string
	stream      string
	expectedKey string
}{{
	`non empty stream`,
	"stream",
	"stream",
}, {
	"empty stream",
	"",
	"released",
}}

func (s *keyMetadataSuite) TestCreateMetadataKey(c *gc.C) {
	for i, t := range keyTestData {
		c.Logf("%d: %v", i, t.about)
		key := cloudimagemetadata.StreamKey(t.stream)
		c.Assert(key, gc.Equals, t.expectedKey)
	}
}

type cloudImageMetadataSuite struct {
	testing.BaseSuite

	mongo     *gitjujutesting.MgoInstance
	session   *mgo.Session
	txnRunner jujutxn.Runner

	storage            cloudimagemetadata.Storage
	metadataCollection *mgo.Collection
}

var _ = gc.Suite(&cloudImageMetadataSuite{})

func (s *cloudImageMetadataSuite) SetUpTest(c *gc.C) {
	s.BaseSuite.SetUpTest(c)
	s.mongo = &gitjujutesting.MgoInstance{}
	s.mongo.Start(nil)

	var err error
	s.session, err = s.mongo.Dial()
	c.Assert(err, jc.ErrorIsNil)
	catalogue := s.session.DB("catalogue")
	s.metadataCollection = catalogue.C("cloudimagemetadata")

	s.txnRunner = jujutxn.NewRunner(jujutxn.RunnerParams{Database: catalogue})
	s.storage = cloudimagemetadata.NewStorage("my-uuid", s.metadataCollection, s.txnRunner)
}

func (s *cloudImageMetadataSuite) TearDownTest(c *gc.C) {
	s.session.Close()
	s.mongo.DestroyWithLog()
	s.BaseSuite.TearDownTest(c)
}

func (s *cloudImageMetadataSuite) TestSaveMetadata(c *gc.C) {
	s.assertSaveMetadataWithDefaults(c, "stream", "series", "arch")
}

func (s *cloudImageMetadataSuite) TestSaveMetadataUpdates(c *gc.C) {
	s.assertSaveMetadataWithDefaults(c, "stream", "series", "arch")
	s.assertSaveMetadata(c, "stream", "region-test", "series",
		"arch", "virtual-type-test", "root-storage-type-test")
}

func (s *cloudImageMetadataSuite) assertSaveMetadataWithDefaults(c *gc.C, stream, series, arch string) {
	s.assertSaveMetadata(c, stream, "region", series, arch, "virtType", "rootType")
}

func (s *cloudImageMetadataSuite) assertSaveMetadata(c *gc.C, stream, region, series, arch, virtType, rootStorageType string) {
	attrs := cloudimagemetadata.MetadataAttributes{
		Stream:          stream,
		Region:          region,
		Series:          series,
		Arch:            arch,
		VirtualType:     virtType,
		RootStorageType: rootStorageType}

	added := cloudimagemetadata.Metadata{attrs, "1"}
	err := s.storage.SaveMetadata(added)
	c.Assert(err, jc.ErrorIsNil)

	s.assertMetadata(c, attrs, added)
}

func (s *cloudImageMetadataSuite) TestAllMetadata(c *gc.C) {
	metadata, err := s.storage.AllMetadata()
	c.Assert(err, jc.ErrorIsNil)
	c.Assert(metadata, gc.HasLen, 0)

	m := cloudimagemetadata.Metadata{
		cloudimagemetadata.MetadataAttributes{
			Stream:          "stream",
			Region:          "region",
			Series:          "series",
			Arch:            "arch",
			VirtualType:     "virtualType",
			RootStorageType: "rootStorageType"},
		"1",
	}

	s.addMetadataDoc(c, m)

	metadata, err = s.storage.AllMetadata()
	c.Assert(err, jc.ErrorIsNil)
	c.Assert(metadata, gc.HasLen, 1)
	expected := []cloudimagemetadata.Metadata{m}
	c.Assert(metadata, jc.SameContents, expected)

	m.Series = "series2"
	s.addMetadataDoc(c, m)

	metadata, err = s.storage.AllMetadata()
	c.Assert(err, jc.ErrorIsNil)
	c.Assert(metadata, gc.HasLen, 2)
	expected = append(expected, m)
	c.Assert(metadata, jc.SameContents, expected)
}

func (s *cloudImageMetadataSuite) TestFindMetadata(c *gc.C) {
	attrs := cloudimagemetadata.MetadataAttributes{
		Stream:          "stream",
		Region:          "region",
		Series:          "series",
		Arch:            "arch",
		VirtualType:     "virtualType",
		RootStorageType: "rootStorageType"}

	m := cloudimagemetadata.Metadata{attrs, "1"}

	_, err := s.storage.FindMetadata(attrs)
	c.Assert(err, jc.Satisfies, errors.IsNotFound)

	s.addMetadataDoc(c, m)
	expected := []cloudimagemetadata.Metadata{m}
	s.assertMetadata(c, attrs, expected...)

	attrs.Stream = "another_stream"
	m = cloudimagemetadata.Metadata{attrs, "2"}
	s.addMetadataDoc(c, m)

	expected = append(expected, m)
	// Should find both
	s.assertMetadata(c, cloudimagemetadata.MetadataAttributes{Region: "region"}, expected...)
}

func (s *cloudImageMetadataSuite) TestSaveMetadataDuplicate(c *gc.C) {
	attrs := cloudimagemetadata.MetadataAttributes{
		Stream: "stream",
		Series: "series",
		Arch:   "arch"}
	metadata := cloudimagemetadata.Metadata{attrs, "1"}

	for i := 0; i < 2; i++ {
		err := s.storage.SaveMetadata(metadata)
		c.Assert(err, jc.ErrorIsNil)
		s.assertMetadata(c, attrs, metadata)
	}
	all, err := s.storage.AllMetadata()
	c.Assert(err, jc.ErrorIsNil)
	c.Assert(all, gc.HasLen, 1)
	expected := []cloudimagemetadata.Metadata{metadata}
	c.Assert(all, jc.SameContents, expected)

}

func (s *cloudImageMetadataSuite) TestSaveMetadataConcurrent(c *gc.C) {
	attrs := cloudimagemetadata.MetadataAttributes{
		Stream: "stream",
		Series: "series",
		Arch:   "arch",
	}
	metadata0 := cloudimagemetadata.Metadata{attrs, "1"}
	metadata1 := cloudimagemetadata.Metadata{attrs, "2"}

	addMetadata := func() {
		err := s.storage.SaveMetadata(metadata0)
		c.Assert(err, jc.ErrorIsNil)
	}
	defer txntesting.SetBeforeHooks(c, s.txnRunner, addMetadata).Check()

	err := s.storage.SaveMetadata(metadata1)
	c.Assert(err, jc.ErrorIsNil)

	s.assertMetadata(c, attrs, metadata1)
}

func (s *cloudImageMetadataSuite) addMetadataDoc(c *gc.C, m cloudimagemetadata.Metadata) {
	doc := createTestDoc(m)
	err := s.metadataCollection.Insert(&doc)
	c.Assert(err, jc.ErrorIsNil)
}

func (s *cloudImageMetadataSuite) assertMetadata(c *gc.C, criteria cloudimagemetadata.MetadataAttributes, expected ...cloudimagemetadata.Metadata) {
	var docs []testMetadataDoc
	searchCriteria := cloudimagemetadata.SearchClauses(criteria)
	c.Logf("looking for cloud image metadata with id %v", criteria)
	err := s.metadataCollection.Find(searchCriteria).All(&docs)
	c.Assert(err, jc.ErrorIsNil)

	metadata := make([]cloudimagemetadata.Metadata, len(docs))
	for i, m := range docs {
		metadata[i] = cloudimagemetadata.Metadata{
			cloudimagemetadata.MetadataAttributes{
				Stream:          m.Stream,
				Region:          m.Region,
				Series:          m.Series,
				Arch:            m.Arch,
				VirtualType:     m.VirtualType,
				RootStorageType: m.RootStorageType,
			}, m.ImageId}
	}
	c.Assert(metadata, jc.SameContents, expected)
}

type testMetadataDoc struct {
	Id              string `bson:"_id"`
	Stream          string `bson:"stream"`
	Region          string `bson:"region`
	Series          string `bson:"series"`
	Arch            string `bson:"arch"`
	ImageId         string `bson:"image_id"`
	VirtualType     string `bson:"virtual_type,omitempty"`
	RootStorageType string `bson:"root_storage_type,omitempty"`
}

func createTestDoc(m cloudimagemetadata.Metadata) testMetadataDoc {
	key := cloudimagemetadata.Key(&m)
	return testMetadataDoc{
		Id:              key,
		Stream:          m.Stream,
		Region:          m.Region,
		Series:          m.Series,
		Arch:            m.Arch,
		VirtualType:     m.VirtualType,
		RootStorageType: m.RootStorageType,
		ImageId:         m.ImageId,
	}
}

type errorTransactionRunner struct {
	jujutxn.Runner
}

func (errorTransactionRunner) Run(transactions jujutxn.TransactionSource) error {
	return errors.New("Run fails")
}
<commit_msg>Added tests for search clause construction.<commit_after>// Copyright 2015 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package cloudimagemetadata_test

import (
	"github.com/juju/errors"
	gitjujutesting "github.com/juju/testing"
	jc "github.com/juju/testing/checkers"
	jujutxn "github.com/juju/txn"
	txntesting "github.com/juju/txn/testing"
	gc "gopkg.in/check.v1"
	"gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"

	"github.com/juju/juju/state/cloudimagemetadata"
	"github.com/juju/juju/testing"
)

type funcMetadataSuite struct {
	testing.BaseSuite
}

var _ = gc.Suite(&funcMetadataSuite{})

var keyTestData = []struct {
	about       string
	stream      string
	expectedKey string
}{{
	`non empty stream`,
	"stream",
	"stream",
}, {
	"empty stream",
	"",
	"released",
}}

func (s *funcMetadataSuite) TestCreateMetadataKey(c *gc.C) {
	for i, t := range keyTestData {
		c.Logf("%d: %v", i, t.about)
		key := cloudimagemetadata.StreamKey(t.stream)
		c.Assert(key, gc.Equals, t.expectedKey)
	}
}

var searchTestData = []struct {
	about    string
	criteria cloudimagemetadata.MetadataAttributes
	expected bson.D
}{{
	`empty criteria`,
	cloudimagemetadata.MetadataAttributes{},
	nil,
}, {
	`only stream supplied`,
	cloudimagemetadata.MetadataAttributes{Stream: "stream-value"},
	bson.D{{"stream", "stream-value"}},
}, {
	`only region supplied`,
	cloudimagemetadata.MetadataAttributes{Region: "region-value"},
	bson.D{{"region", "region-value"}},
}, {
	`only series supplied`,
	cloudimagemetadata.MetadataAttributes{Series: "series-value"},
	bson.D{{"series", "series-value"}},
}, {
	`only arch supplied`,
	cloudimagemetadata.MetadataAttributes{Arch: "arch-value"},
	bson.D{{"arch", "arch-value"}},
}, {
	`only virtual type supplied`,
	cloudimagemetadata.MetadataAttributes{VirtualType: "vtype-value"},
	bson.D{{"virtual_type", "vtype-value"}},
}, {
	`only root storage type supplied`,
	cloudimagemetadata.MetadataAttributes{RootStorageType: "rootstorage-value"},
	bson.D{{"root_storage_type", "rootstorage-value"}},
}, {
	`two search criteria supplied`,
	cloudimagemetadata.MetadataAttributes{RootStorageType: "rootstorage-value", Series: "series-value"},
	bson.D{{"root_storage_type", "rootstorage-value"}, {"series", "series-value"}},
}, {
	`all serach criteria supplied`,
	cloudimagemetadata.MetadataAttributes{
		RootStorageType: "rootstorage-value",
		Series:          "series-value",
		Stream:          "stream-value",
		Region:          "region-value",
		Arch:            "arch-value",
		VirtualType:     "vtype-value",
	},
	bson.D{
		{"root_storage_type", "rootstorage-value"},
		{"series", "series-value"},
		{"stream", "stream-value"},
		{"region", "region-value"},
		{"arch", "arch-value"},
		{"virtual_type", "vtype-value"},
	},
}}

func (s *funcMetadataSuite) TestSearchCriteria(c *gc.C) {
	for i, t := range searchTestData {
		c.Logf("%d: %v", i, t.about)
		clause := cloudimagemetadata.SearchClauses(t.criteria)
		c.Assert(clause, jc.SameContents, t.expected)
	}
}

type cloudImageMetadataSuite struct {
	testing.BaseSuite

	mongo     *gitjujutesting.MgoInstance
	session   *mgo.Session
	txnRunner jujutxn.Runner

	storage            cloudimagemetadata.Storage
	metadataCollection *mgo.Collection
}

var _ = gc.Suite(&cloudImageMetadataSuite{})

func (s *cloudImageMetadataSuite) SetUpTest(c *gc.C) {
	s.BaseSuite.SetUpTest(c)
	s.mongo = &gitjujutesting.MgoInstance{}
	s.mongo.Start(nil)

	var err error
	s.session, err = s.mongo.Dial()
	c.Assert(err, jc.ErrorIsNil)
	catalogue := s.session.DB("catalogue")
	s.metadataCollection = catalogue.C("cloudimagemetadata")

	s.txnRunner = jujutxn.NewRunner(jujutxn.RunnerParams{Database: catalogue})
	s.storage = cloudimagemetadata.NewStorage("my-uuid", s.metadataCollection, s.txnRunner)
}

func (s *cloudImageMetadataSuite) TearDownTest(c *gc.C) {
	s.session.Close()
	s.mongo.DestroyWithLog()
	s.BaseSuite.TearDownTest(c)
}

func (s *cloudImageMetadataSuite) TestSaveMetadata(c *gc.C) {
	s.assertSaveMetadataWithDefaults(c, "stream", "series", "arch")
}

func (s *cloudImageMetadataSuite) TestSaveMetadataUpdates(c *gc.C) {
	s.assertSaveMetadataWithDefaults(c, "stream", "series", "arch")
	s.assertSaveMetadata(c, "stream", "region-test", "series",
		"arch", "virtual-type-test", "root-storage-type-test")
}

func (s *cloudImageMetadataSuite) assertSaveMetadataWithDefaults(c *gc.C, stream, series, arch string) {
	s.assertSaveMetadata(c, stream, "region", series, arch, "virtType", "rootType")
}

func (s *cloudImageMetadataSuite) assertSaveMetadata(c *gc.C, stream, region, series, arch, virtType, rootStorageType string) {
	attrs := cloudimagemetadata.MetadataAttributes{
		Stream:          stream,
		Region:          region,
		Series:          series,
		Arch:            arch,
		VirtualType:     virtType,
		RootStorageType: rootStorageType}

	added := cloudimagemetadata.Metadata{attrs, "1"}
	err := s.storage.SaveMetadata(added)
	c.Assert(err, jc.ErrorIsNil)

	s.assertMetadata(c, attrs, added)
}

func (s *cloudImageMetadataSuite) TestAllMetadata(c *gc.C) {
	metadata, err := s.storage.AllMetadata()
	c.Assert(err, jc.ErrorIsNil)
	c.Assert(metadata, gc.HasLen, 0)

	m := cloudimagemetadata.Metadata{
		cloudimagemetadata.MetadataAttributes{
			Stream:          "stream",
			Region:          "region",
			Series:          "series",
			Arch:            "arch",
			VirtualType:     "virtualType",
			RootStorageType: "rootStorageType"},
		"1",
	}

	s.addMetadataDoc(c, m)

	metadata, err = s.storage.AllMetadata()
	c.Assert(err, jc.ErrorIsNil)
	c.Assert(metadata, gc.HasLen, 1)
	expected := []cloudimagemetadata.Metadata{m}
	c.Assert(metadata, jc.SameContents, expected)

	m.Series = "series2"
	s.addMetadataDoc(c, m)

	metadata, err = s.storage.AllMetadata()
	c.Assert(err, jc.ErrorIsNil)
	c.Assert(metadata, gc.HasLen, 2)
	expected = append(expected, m)
	c.Assert(metadata, jc.SameContents, expected)
}

func (s *cloudImageMetadataSuite) TestFindMetadata(c *gc.C) {
	attrs := cloudimagemetadata.MetadataAttributes{
		Stream:          "stream",
		Region:          "region",
		Series:          "series",
		Arch:            "arch",
		VirtualType:     "virtualType",
		RootStorageType: "rootStorageType"}

	m := cloudimagemetadata.Metadata{attrs, "1"}

	_, err := s.storage.FindMetadata(attrs)
	c.Assert(err, jc.Satisfies, errors.IsNotFound)

	s.addMetadataDoc(c, m)
	expected := []cloudimagemetadata.Metadata{m}
	s.assertMetadata(c, attrs, expected...)

	attrs.Stream = "another_stream"
	m = cloudimagemetadata.Metadata{attrs, "2"}
	s.addMetadataDoc(c, m)

	expected = append(expected, m)
	// Should find both
	s.assertMetadata(c, cloudimagemetadata.MetadataAttributes{Region: "region"}, expected...)
}

func (s *cloudImageMetadataSuite) TestSaveMetadataDuplicate(c *gc.C) {
	attrs := cloudimagemetadata.MetadataAttributes{
		Stream: "stream",
		Series: "series",
		Arch:   "arch"}
	metadata := cloudimagemetadata.Metadata{attrs, "1"}

	for i := 0; i < 2; i++ {
		err := s.storage.SaveMetadata(metadata)
		c.Assert(err, jc.ErrorIsNil)
		s.assertMetadata(c, attrs, metadata)
	}
	all, err := s.storage.AllMetadata()
	c.Assert(err, jc.ErrorIsNil)
	c.Assert(all, gc.HasLen, 1)
	expected := []cloudimagemetadata.Metadata{metadata}
	c.Assert(all, jc.SameContents, expected)

}

func (s *cloudImageMetadataSuite) TestSaveMetadataConcurrent(c *gc.C) {
	attrs := cloudimagemetadata.MetadataAttributes{
		Stream: "stream",
		Series: "series",
		Arch:   "arch",
	}
	metadata0 := cloudimagemetadata.Metadata{attrs, "1"}
	metadata1 := cloudimagemetadata.Metadata{attrs, "2"}

	addMetadata := func() {
		err := s.storage.SaveMetadata(metadata0)
		c.Assert(err, jc.ErrorIsNil)
	}
	defer txntesting.SetBeforeHooks(c, s.txnRunner, addMetadata).Check()

	err := s.storage.SaveMetadata(metadata1)
	c.Assert(err, jc.ErrorIsNil)

	s.assertMetadata(c, attrs, metadata1)
}

func (s *cloudImageMetadataSuite) addMetadataDoc(c *gc.C, m cloudimagemetadata.Metadata) {
	doc := createTestDoc(m)
	err := s.metadataCollection.Insert(&doc)
	c.Assert(err, jc.ErrorIsNil)
}

func (s *cloudImageMetadataSuite) assertMetadata(c *gc.C, criteria cloudimagemetadata.MetadataAttributes, expected ...cloudimagemetadata.Metadata) {
	var docs []testMetadataDoc
	searchCriteria := cloudimagemetadata.SearchClauses(criteria)
	c.Logf("looking for cloud image metadata with id %v", criteria)
	err := s.metadataCollection.Find(searchCriteria).All(&docs)
	c.Assert(err, jc.ErrorIsNil)

	metadata := make([]cloudimagemetadata.Metadata, len(docs))
	for i, m := range docs {
		metadata[i] = cloudimagemetadata.Metadata{
			cloudimagemetadata.MetadataAttributes{
				Stream:          m.Stream,
				Region:          m.Region,
				Series:          m.Series,
				Arch:            m.Arch,
				VirtualType:     m.VirtualType,
				RootStorageType: m.RootStorageType,
			}, m.ImageId}
	}
	c.Assert(metadata, jc.SameContents, expected)
}

type testMetadataDoc struct {
	Id              string `bson:"_id"`
	Stream          string `bson:"stream"`
	Region          string `bson:"region`
	Series          string `bson:"series"`
	Arch            string `bson:"arch"`
	ImageId         string `bson:"image_id"`
	VirtualType     string `bson:"virtual_type,omitempty"`
	RootStorageType string `bson:"root_storage_type,omitempty"`
}

func createTestDoc(m cloudimagemetadata.Metadata) testMetadataDoc {
	key := cloudimagemetadata.Key(&m)
	return testMetadataDoc{
		Id:              key,
		Stream:          m.Stream,
		Region:          m.Region,
		Series:          m.Series,
		Arch:            m.Arch,
		VirtualType:     m.VirtualType,
		RootStorageType: m.RootStorageType,
		ImageId:         m.ImageId,
	}
}

type errorTransactionRunner struct {
	jujutxn.Runner
}

func (errorTransactionRunner) Run(transactions jujutxn.TransactionSource) error {
	return errors.New("Run fails")
}
<|endoftext|>


<commit_before>// Expression problem solution in Go with interfaces.
//
// Eli Bendersky [http://eli.thegreenplace.net]
// This code is in the public domain.

package main

import (
	"fmt"
	"strconv"
)

type Expr interface {
}

type Constant struct {
	value float64
}

type BinaryPlus struct {
	left  Expr
	right Expr
}

type Eval interface {
	Eval() float64
}

type Stringify interface {
	ToString() string
}

func (c *Constant) Eval() float64 {
	return c.value
}

func (bp *BinaryPlus) Eval() float64 {
	return bp.left.(Eval).Eval() + bp.right.(Eval).Eval()
}

func (c *Constant) ToString() string {
	return strconv.FormatFloat(c.value, 'f', -1, 64)
}

func (bp *BinaryPlus) ToString() string {
	// The moment of truth is here... bp.left is an Expr, which does not
	// have a ToString method. Obviously this will only work if left and right
	// implement the Stringable interface. The type assertion makes this
	// expectation explicit and will panic otherwise.
	ls := bp.left.(Stringify)
	rs := bp.right.(Stringify)
	return fmt.Sprintf("(%s + %s)", ls.ToString(), rs.ToString())
}

// Now adding a new node...

type BinaryMul struct {
	left  Expr
	right Expr
}

func (bm *BinaryMul) Eval() float64 {
	return bm.left.(Eval).Eval() * bm.right.(Eval).Eval()
}

func (bm *BinaryMul) ToString() string {
	ls := bm.left.(Stringify)
	rs := bm.right.(Stringify)
	return fmt.Sprintf("(%s * %s)", ls.ToString(), rs.ToString())
}

func main() {
	fmt.Println("booya")

	// constants
	c := Constant{value: 26.4}

	fmt.Printf("c Eval = %g\n", c.Eval())
	fmt.Printf("c ToString = %s\n", c.ToString())

	c11 := Constant{value: 1.1}
	c22 := Constant{value: 2.2}
	c33 := Constant{value: 3.3}
	bp := BinaryPlus{left: &BinaryPlus{left: &c11, right: &c22}, right: &c33}

	fmt.Printf("bp Eval = %g\n", bp.Eval())
	fmt.Printf("bp ToString = %s\n", bp.ToString())

	bm := BinaryMul{left: &bp, right: &c22}

	fmt.Printf("bm Eval = %g\n", bm.Eval())
	fmt.Printf("bm ToString = %s\n", bm.ToString())
}
<commit_msg>Clean up and add CreateNewExpr to demonstrate creation<commit_after>// Expression problem solution in Go with interfaces.
//
// Eli Bendersky [http://eli.thegreenplace.net]
// This code is in the public domain.

package main

import (
	"fmt"
	"strconv"
)

type Expr interface {
}

// Types
type Constant struct {
	value float64
}

type BinaryPlus struct {
	left  Expr
	right Expr
}

// Functions wrapped in interfaces
type Eval interface {
	Eval() float64
}

type Stringify interface {
	ToString() string
}

func (c *Constant) Eval() float64 {
	return c.value
}

func (c *Constant) ToString() string {
	return strconv.FormatFloat(c.value, 'f', -1, 64)
}

// As far as the compiler is concerned, bp.left is an Expr. Expr doesn't have an
// Eval method. Therefore, a cast is required - it will fail at runtime if
// bp.left doesn't implement Eval.
func (bp *BinaryPlus) Eval() float64 {
	return bp.left.(Eval).Eval() + bp.right.(Eval).Eval()
}

func (bp *BinaryPlus) ToString() string {
	ls := bp.left.(Stringify)
	rs := bp.right.(Stringify)
	return fmt.Sprintf("(%s + %s)", ls.ToString(), rs.ToString())
}

// Now adding a new type...

type BinaryMul struct {
	left  Expr
	right Expr
}

func (bm *BinaryMul) Eval() float64 {
	return bm.left.(Eval).Eval() * bm.right.(Eval).Eval()
}

func (bm *BinaryMul) ToString() string {
	ls := bm.left.(Stringify)
	rs := bm.right.(Stringify)
	return fmt.Sprintf("(%s * %s)", ls.ToString(), rs.ToString())
}

// Function that emulates creating a new expression from some input. It has to
// return Expr, which should then be casted with a type assertion.
func CreateNewExpr() Expr {
	c11 := Constant{value: 1.1}
	c22 := Constant{value: 2.2}
	c33 := Constant{value: 3.3}
	bp := BinaryPlus{left: &BinaryPlus{left: &c11, right: &c22}, right: &c33}
	return &bp
}

func main() {
	fmt.Println("hello")

	// constants
	c := Constant{value: 26.4}

	fmt.Printf("c Eval = %g\n", c.Eval())
	fmt.Printf("c ToString = %s\n", c.ToString())

	c11 := Constant{value: 1.1}
	c22 := Constant{value: 2.2}
	c33 := Constant{value: 3.3}
	bp := BinaryPlus{left: &BinaryPlus{left: &c11, right: &c22}, right: &c33}

	fmt.Printf("bp Eval = %g\n", bp.Eval())
	fmt.Printf("bp ToString = %s\n", bp.ToString())

	ne := CreateNewExpr()
	fmt.Printf("ne Eval = %g\n", ne.(Eval).Eval())
	fmt.Printf("ne ToString = %s\n", ne.(Stringify).ToString())

	bm := BinaryMul{left: &bp, right: &c22}

	fmt.Printf("bm Eval = %g\n", bm.Eval())
	fmt.Printf("bm ToString = %s\n", bm.ToString())
}
<|endoftext|>


<commit_before>package main

import (
	"time"

	"fmt"

	"github.com/JessonChan/yorm"
)

/*
CREATE TABLE `program_language` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  `rank_month` date DEFAULT NULL,
  `position` int(11) DEFAULT NULL,
  `created` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
*/

type ProgramLanguage struct {
	Id        int64
	Position  int
	Name      string
	RankMonth time.Time
	Created   time.Time
}

func main() {
	yorm.SetLoggerLevel(yorm.DebugLevel)
	//设置自己的数据地址
	yorm.Register("root:@tcp(127.0.0.1:3306)/yorm_test?charset=utf8")

	//插入一条数据
	php := ProgramLanguage{Name: "PHP", Position: 7, RankMonth: time.Now(), Created: time.Now()}
	yorm.Insert(&php)

	var ps []ProgramLanguage

	//读取所有的数据
	yorm.R(&ps)
	fmt.Println(ps)

	//读取所有小于10的数据
	yorm.R(&ps, "where id<10")
	fmt.Println(ps)

	//读取所有小于10的数据
	yorm.R(&ps, "where id<?", 10)
	fmt.Println(ps)

	//也可以
	yorm.Select(&ps, "where id<?", 10)
	fmt.Println(ps)

	//也可以
	yorm.Select(&ps, "where <10")
	fmt.Println(ps)

	var p ProgramLanguage = ProgramLanguage{Id: 1}

	//读取id为1的某条数据
	yorm.R(&p)
	fmt.Println(p)

	//读取id为1的某条数据
	yorm.SelectByPK(&p)
	fmt.Println(p)

	//读取id为2的某条数据,
	yorm.SelectByPK(&p, "where id=?", 2)
	fmt.Println(p)

	//更新一条数据
	yorm.Update("update program_language set position=?", 8)

	//删除一条
	yorm.Delete("delete from program_language where id=? ", p.Id)
}
<commit_msg>insert 方法<commit_after>package main

import (
	"time"

	"fmt"

	"github.com/JessonChan/yorm"
)

/*
CREATE TABLE `program_language` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  `rank_month` date DEFAULT NULL,
  `position` int(11) DEFAULT NULL,
  `created` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
*/

type ProgramLanguage struct {
	Id        int64
	Position  int
	Name      string
	RankMonth time.Time
	Created   time.Time
}

func main() {
	yorm.SetLoggerLevel(yorm.DebugLevel)
	//设置自己的数据地址
	yorm.Register("root:@tcp(127.0.0.1:3306)/yorm_test?charset=utf8")

	//插入一条数据
	php := ProgramLanguage{Name: "PHP", Position: 7, RankMonth: time.Now(), Created: time.Now()}
	yorm.Insert(&php)

	//更新一条数据
	yorm.Update("update program_language set position=? where id=?", 8, php.Id)

	//删除一条
	yorm.Delete("delete from program_language where id=? ", php.Id)

	var ps []ProgramLanguage

	//读取所有的数据
	yorm.R(&ps)
	fmt.Println(ps)

	//读取所有小于10的数据
	yorm.R(&ps, "where id<10")
	fmt.Println(ps)

	//读取所有小于10的数据
	yorm.R(&ps, "where id<?", 10)
	fmt.Println(ps)

	//也可以
	yorm.Select(&ps, "where id<?", 10)
	fmt.Println(ps)

	//也可以
	yorm.Select(&ps, "where <10")
	fmt.Println(ps)

	var p ProgramLanguage = ProgramLanguage{Id: 1}

	//读取id为1的某条数据
	yorm.R(&p)
	fmt.Println(p)

	//读取id为1的某条数据
	yorm.SelectByPK(&p)
	fmt.Println(p)

	//读取id为2的某条数据,
	yorm.SelectByPK(&p, "where id=?", 2)
	fmt.Println(p)

}
<|endoftext|>


<commit_before>// Copyright 2017 The casbin Authors. 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 xormadapter

import (
	"errors"
	"runtime"

	"github.com/casbin/casbin/model"
	"github.com/casbin/casbin/persist"
	"github.com/go-xorm/xorm"
	"github.com/lib/pq"
)

type CasbinRule struct {
	PType string `xorm:"varchar(100)"`
	V0    string `xorm:"varchar(100)"`
	V1    string `xorm:"varchar(100)"`
	V2    string `xorm:"varchar(100)"`
	V3    string `xorm:"varchar(100)"`
	V4    string `xorm:"varchar(100)"`
	V5    string `xorm:"varchar(100)"`
}

// Adapter represents the Xorm adapter for policy storage.
type Adapter struct {
	driverName     string
	dataSourceName string
	dbSpecified    bool
	engine         *xorm.Engine
}

// finalizer is the destructor for Adapter.
func finalizer(a *Adapter) {
	a.engine.Close()
}

// NewAdapter is the constructor for Adapter.
// dbSpecified is an optional bool parameter. The default value is false.
// It's up to whether you have specified an existing DB in dataSourceName.
// If dbSpecified == true, you need to make sure the DB in dataSourceName exists.
// If dbSpecified == false, the adapter will automatically create a DB named "casbin".
func NewAdapter(driverName string, dataSourceName string, dbSpecified ...bool) *Adapter {
	a := &Adapter{}
	a.driverName = driverName
	a.dataSourceName = dataSourceName

	if len(dbSpecified) == 0 {
		a.dbSpecified = false
	} else if len(dbSpecified) == 1 {
		a.dbSpecified = dbSpecified[0]
	} else {
		panic(errors.New("invalid parameter: dbSpecified"))
	}

	// Open the DB, create it if not existed.
	a.open()

	// Call the destructor when the object is released.
	runtime.SetFinalizer(a, finalizer)

	return a
}

func (a *Adapter) createDatabase() error {
	var err error
	var engine *xorm.Engine
	if a.driverName == "postgres" {
		engine, err = xorm.NewEngine(a.driverName, a.dataSourceName+" dbname=postgres")
	} else {
		engine, err = xorm.NewEngine(a.driverName, a.dataSourceName)
	}
	if err != nil {
		return err
	}
	defer engine.Close()

	if a.driverName == "postgres" {
		if _, err = engine.Exec("CREATE DATABASE casbin"); err != nil {
			// 42P04 is	duplicate_database
			if err.(*pq.Error).Code == "42P04" {
				return nil
			}
		}
	} else {
		_, err = engine.Exec("CREATE DATABASE IF NOT EXISTS casbin")
	}
	return err
}

func (a *Adapter) open() {
	var err error
	var engine *xorm.Engine

	if a.dbSpecified {
		engine, err = xorm.NewEngine(a.driverName, a.dataSourceName)
		if err != nil {
			panic(err)
		}
	} else {
		if err = a.createDatabase(); err != nil {
			panic(err)
		}

		if a.driverName == "postgres" {
			engine, err = xorm.NewEngine(a.driverName, a.dataSourceName+" dbname=casbin")
		} else {
			engine, err = xorm.NewEngine(a.driverName, a.dataSourceName+"casbin")
		}
		if err != nil {
			panic(err)
		}
	}

	a.engine = engine

	a.createTable()
}

func (a *Adapter) close() {
	a.engine.Close()
	a.engine = nil
}

func (a *Adapter) createTable() {
	err := a.engine.Sync2(new(CasbinRule))
	if err != nil {
		panic(err)
	}
}

func (a *Adapter) dropTable() {
	err := a.engine.DropTables(new(CasbinRule))
	if err != nil {
		panic(err)
	}
}

func loadPolicyLine(line CasbinRule, model model.Model) {
	lineText := line.PType
	if line.V0 != "" {
		lineText += ", " + line.V0
	}
	if line.V1 != "" {
		lineText += ", " + line.V1
	}
	if line.V2 != "" {
		lineText += ", " + line.V2
	}
	if line.V3 != "" {
		lineText += ", " + line.V3
	}
	if line.V4 != "" {
		lineText += ", " + line.V4
	}
	if line.V5 != "" {
		lineText += ", " + line.V5
	}

	persist.LoadPolicyLine(lineText, model)
}

// LoadPolicy loads policy from database.
func (a *Adapter) LoadPolicy(model model.Model) error {
	var lines []CasbinRule
	err := a.engine.Find(&lines)
	if err != nil {
		return err
	}

	for _, line := range lines {
		loadPolicyLine(line, model)
	}

	return nil
}

func savePolicyLine(ptype string, rule []string) CasbinRule {
	line := CasbinRule{}

	line.PType = ptype
	if len(rule) > 0 {
		line.V0 = rule[0]
	}
	if len(rule) > 1 {
		line.V1 = rule[1]
	}
	if len(rule) > 2 {
		line.V2 = rule[2]
	}
	if len(rule) > 3 {
		line.V3 = rule[3]
	}
	if len(rule) > 4 {
		line.V4 = rule[4]
	}
	if len(rule) > 5 {
		line.V5 = rule[5]
	}

	return line
}

// SavePolicy saves policy to database.
func (a *Adapter) SavePolicy(model model.Model) error {
	a.dropTable()
	a.createTable()

	var lines []CasbinRule

	for ptype, ast := range model["p"] {
		for _, rule := range ast.Policy {
			line := savePolicyLine(ptype, rule)
			lines = append(lines, line)
		}
	}

	for ptype, ast := range model["g"] {
		for _, rule := range ast.Policy {
			line := savePolicyLine(ptype, rule)
			lines = append(lines, line)
		}
	}

	_, err := a.engine.Insert(&lines)
	return err
}

// AddPolicy adds a policy rule to the storage.
func (a *Adapter) AddPolicy(sec string, ptype string, rule []string) error {
	line := savePolicyLine(ptype, rule)
	_, err := a.engine.Insert(line)
	return err
}

// RemovePolicy removes a policy rule from the storage.
func (a *Adapter) RemovePolicy(sec string, ptype string, rule []string) error {
	line := savePolicyLine(ptype, rule)
	_, err := a.engine.Delete(line)
	return err
}

// RemoveFilteredPolicy removes policy rules that match the filter from the storage.
func (a *Adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
	line := CasbinRule{}

	line.PType = ptype
	if fieldIndex <= 0 && 0 < fieldIndex + len(fieldValues) {
		line.V0 = fieldValues[0 - fieldIndex]
	}
	if fieldIndex <= 1 && 1 < fieldIndex + len(fieldValues) {
		line.V1 = fieldValues[1 - fieldIndex]
	}
	if fieldIndex <= 2 && 2 < fieldIndex + len(fieldValues) {
		line.V2 = fieldValues[2 - fieldIndex]
	}
	if fieldIndex <= 3 && 3 < fieldIndex + len(fieldValues) {
		line.V3 = fieldValues[3 - fieldIndex]
	}
	if fieldIndex <= 4 && 4 < fieldIndex + len(fieldValues) {
		line.V4 = fieldValues[4 - fieldIndex]
	}
	if fieldIndex <= 5 && 5 < fieldIndex + len(fieldValues) {
		line.V5 = fieldValues[5 - fieldIndex]
	}

	_, err := a.engine.Delete(line)
	return err
}
<commit_msg>Add SQL index to gain speed.<commit_after>// Copyright 2017 The casbin Authors. 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 xormadapter

import (
	"errors"
	"runtime"

	"github.com/casbin/casbin/model"
	"github.com/casbin/casbin/persist"
	"github.com/go-xorm/xorm"
	"github.com/lib/pq"
)

type CasbinRule struct {
	PType string `xorm:"varchar(100) index"`
	V0    string `xorm:"varchar(100) index"`
	V1    string `xorm:"varchar(100) index"`
	V2    string `xorm:"varchar(100) index"`
	V3    string `xorm:"varchar(100) index"`
	V4    string `xorm:"varchar(100) index"`
	V5    string `xorm:"varchar(100) index"`
}

// Adapter represents the Xorm adapter for policy storage.
type Adapter struct {
	driverName     string
	dataSourceName string
	dbSpecified    bool
	engine         *xorm.Engine
}

// finalizer is the destructor for Adapter.
func finalizer(a *Adapter) {
	a.engine.Close()
}

// NewAdapter is the constructor for Adapter.
// dbSpecified is an optional bool parameter. The default value is false.
// It's up to whether you have specified an existing DB in dataSourceName.
// If dbSpecified == true, you need to make sure the DB in dataSourceName exists.
// If dbSpecified == false, the adapter will automatically create a DB named "casbin".
func NewAdapter(driverName string, dataSourceName string, dbSpecified ...bool) *Adapter {
	a := &Adapter{}
	a.driverName = driverName
	a.dataSourceName = dataSourceName

	if len(dbSpecified) == 0 {
		a.dbSpecified = false
	} else if len(dbSpecified) == 1 {
		a.dbSpecified = dbSpecified[0]
	} else {
		panic(errors.New("invalid parameter: dbSpecified"))
	}

	// Open the DB, create it if not existed.
	a.open()

	// Call the destructor when the object is released.
	runtime.SetFinalizer(a, finalizer)

	return a
}

func (a *Adapter) createDatabase() error {
	var err error
	var engine *xorm.Engine
	if a.driverName == "postgres" {
		engine, err = xorm.NewEngine(a.driverName, a.dataSourceName+" dbname=postgres")
	} else {
		engine, err = xorm.NewEngine(a.driverName, a.dataSourceName)
	}
	if err != nil {
		return err
	}
	defer engine.Close()

	if a.driverName == "postgres" {
		if _, err = engine.Exec("CREATE DATABASE casbin"); err != nil {
			// 42P04 is	duplicate_database
			if err.(*pq.Error).Code == "42P04" {
				return nil
			}
		}
	} else {
		_, err = engine.Exec("CREATE DATABASE IF NOT EXISTS casbin")
	}
	return err
}

func (a *Adapter) open() {
	var err error
	var engine *xorm.Engine

	if a.dbSpecified {
		engine, err = xorm.NewEngine(a.driverName, a.dataSourceName)
		if err != nil {
			panic(err)
		}
	} else {
		if err = a.createDatabase(); err != nil {
			panic(err)
		}

		if a.driverName == "postgres" {
			engine, err = xorm.NewEngine(a.driverName, a.dataSourceName+" dbname=casbin")
		} else {
			engine, err = xorm.NewEngine(a.driverName, a.dataSourceName+"casbin")
		}
		if err != nil {
			panic(err)
		}
	}

	a.engine = engine

	a.createTable()
}

func (a *Adapter) close() {
	a.engine.Close()
	a.engine = nil
}

func (a *Adapter) createTable() {
	err := a.engine.Sync2(new(CasbinRule))
	if err != nil {
		panic(err)
	}
}

func (a *Adapter) dropTable() {
	err := a.engine.DropTables(new(CasbinRule))
	if err != nil {
		panic(err)
	}
}

func loadPolicyLine(line CasbinRule, model model.Model) {
	lineText := line.PType
	if line.V0 != "" {
		lineText += ", " + line.V0
	}
	if line.V1 != "" {
		lineText += ", " + line.V1
	}
	if line.V2 != "" {
		lineText += ", " + line.V2
	}
	if line.V3 != "" {
		lineText += ", " + line.V3
	}
	if line.V4 != "" {
		lineText += ", " + line.V4
	}
	if line.V5 != "" {
		lineText += ", " + line.V5
	}

	persist.LoadPolicyLine(lineText, model)
}

// LoadPolicy loads policy from database.
func (a *Adapter) LoadPolicy(model model.Model) error {
	var lines []CasbinRule
	err := a.engine.Find(&lines)
	if err != nil {
		return err
	}

	for _, line := range lines {
		loadPolicyLine(line, model)
	}

	return nil
}

func savePolicyLine(ptype string, rule []string) CasbinRule {
	line := CasbinRule{}

	line.PType = ptype
	if len(rule) > 0 {
		line.V0 = rule[0]
	}
	if len(rule) > 1 {
		line.V1 = rule[1]
	}
	if len(rule) > 2 {
		line.V2 = rule[2]
	}
	if len(rule) > 3 {
		line.V3 = rule[3]
	}
	if len(rule) > 4 {
		line.V4 = rule[4]
	}
	if len(rule) > 5 {
		line.V5 = rule[5]
	}

	return line
}

// SavePolicy saves policy to database.
func (a *Adapter) SavePolicy(model model.Model) error {
	a.dropTable()
	a.createTable()

	var lines []CasbinRule

	for ptype, ast := range model["p"] {
		for _, rule := range ast.Policy {
			line := savePolicyLine(ptype, rule)
			lines = append(lines, line)
		}
	}

	for ptype, ast := range model["g"] {
		for _, rule := range ast.Policy {
			line := savePolicyLine(ptype, rule)
			lines = append(lines, line)
		}
	}

	_, err := a.engine.Insert(&lines)
	return err
}

// AddPolicy adds a policy rule to the storage.
func (a *Adapter) AddPolicy(sec string, ptype string, rule []string) error {
	line := savePolicyLine(ptype, rule)
	_, err := a.engine.Insert(line)
	return err
}

// RemovePolicy removes a policy rule from the storage.
func (a *Adapter) RemovePolicy(sec string, ptype string, rule []string) error {
	line := savePolicyLine(ptype, rule)
	_, err := a.engine.Delete(line)
	return err
}

// RemoveFilteredPolicy removes policy rules that match the filter from the storage.
func (a *Adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
	line := CasbinRule{}

	line.PType = ptype
	if fieldIndex <= 0 && 0 < fieldIndex + len(fieldValues) {
		line.V0 = fieldValues[0 - fieldIndex]
	}
	if fieldIndex <= 1 && 1 < fieldIndex + len(fieldValues) {
		line.V1 = fieldValues[1 - fieldIndex]
	}
	if fieldIndex <= 2 && 2 < fieldIndex + len(fieldValues) {
		line.V2 = fieldValues[2 - fieldIndex]
	}
	if fieldIndex <= 3 && 3 < fieldIndex + len(fieldValues) {
		line.V3 = fieldValues[3 - fieldIndex]
	}
	if fieldIndex <= 4 && 4 < fieldIndex + len(fieldValues) {
		line.V4 = fieldValues[4 - fieldIndex]
	}
	if fieldIndex <= 5 && 5 < fieldIndex + len(fieldValues) {
		line.V5 = fieldValues[5 - fieldIndex]
	}

	_, err := a.engine.Delete(line)
	return err
}
<|endoftext|>


<commit_before>package ldaputil

import (
	"fmt"
	"net"
	"net/url"
	"strconv"
	"strings"

	"gopkg.in/ldap.v2"
)

// Scheme is a valid ldap scheme
type Scheme string

const (
	SchemeLDAP  Scheme = "ldap"
	SchemeLDAPS Scheme = "ldaps"
)

// Scope is a valid LDAP search scope
type Scope int

const (
	ScopeWholeSubtree Scope = ldap.ScopeWholeSubtree
	ScopeSingleLevel  Scope = ldap.ScopeSingleLevel
	ScopeBaseObject   Scope = ldap.ScopeBaseObject
)

// DerefAliases is a valid LDAP alias dereference parameter
type DerefAliases int

const (
	DerefAliasesNever     = ldap.NeverDerefAliases
	DerefAliasesSearching = ldap.DerefInSearching
	DerefAliasesFinding   = ldap.DerefFindingBaseObj
	DerefAliasesAlways    = ldap.DerefAlways
)

const (
	defaultLDAPPort  = 389
	defaultLDAPSPort = 636

	defaultHost           = "localhost"
	defaultQueryAttribute = "uid"
	defaultFilter         = "(objectClass=*)"

	scopeWholeSubtreeString = "sub"
	scopeSingleLevelString  = "one"
	scopeBaseObjectString   = "base"

	criticalExtensionPrefix = "!"
)

// LDAPURL holds a parsed RFC 2255 URL
type LDAPURL struct {
	// Scheme is ldap or ldaps
	Scheme Scheme
	// Host is the host:port of the LDAP server
	Host string
	// The DN of the branch of the directory where all searches should start from
	BaseDN string
	// The attribute to search for
	QueryAttribute string
	// The scope of the search. Can be ldap.ScopeWholeSubtree, ldap.ScopeSingleLevel, or ldap.ScopeBaseObject
	Scope Scope
	// A valid LDAP search filter (e.g. "(objectClass=*)")
	Filter string
}

// ParseURL parsed the given ldapURL as an RFC 2255 URL
// The syntax of the URL is ldap://host:port/basedn?attribute?scope?filter
func ParseURL(ldapURL string) (LDAPURL, error) {
	// Must be a valid URL to start
	parsedURL, err := url.Parse(ldapURL)
	if err != nil {
		return LDAPURL{}, err
	}

	opts := LDAPURL{}

	determinedScheme, err := DetermineLDAPScheme(parsedURL.Scheme)
	if err != nil {
		return LDAPURL{}, err
	}
	opts.Scheme = determinedScheme

	determinedHost, err := DetermineLDAPHost(parsedURL.Host, opts.Scheme)
	if err != nil {
		return LDAPURL{}, err
	}
	opts.Host = determinedHost

	// Set base dn (default to "")
	// url.Parse() already percent-decodes the path
	opts.BaseDN = strings.TrimLeft(parsedURL.Path, "/")

	attributes, scope, filter, extensions, err := SplitLDAPQuery(parsedURL.RawQuery)
	if err != nil {
		return LDAPURL{}, err
	}

	// Attributes contains comma-separated attributes
	// Set query attribute to first attribute
	// Default to uid to match mod_auth_ldap
	opts.QueryAttribute = strings.Split(attributes, ",")[0]
	if len(opts.QueryAttribute) == 0 {
		opts.QueryAttribute = defaultQueryAttribute
	}

	determinedScope, err := DetermineLDAPScope(scope)
	if err != nil {
		return LDAPURL{}, err
	}
	opts.Scope = determinedScope

	determinedFilter, err := DetermineLDAPFilter(filter)
	if err != nil {
		return LDAPURL{}, err
	}
	opts.Filter = determinedFilter

	// Extensions are in "name=value,name2=value2" form
	// Critical extensions are prefixed with a !
	// Optional extensions are ignored, per RFC
	// Fail if there are any critical extensions, since we don't support any
	if len(extensions) > 0 {
		for _, extension := range strings.Split(extensions, ",") {
			exttype := strings.SplitN(extension, "=", 2)[0]
			if strings.HasPrefix(exttype, criticalExtensionPrefix) {
				return LDAPURL{}, fmt.Errorf("unsupported critical extension %s", extension)
			}
		}
	}

	return opts, nil

}

// DetermineLDAPScheme determines the LDAP connection scheme. Scheme is one of "ldap" or "ldaps"
// Default to "ldap"
func DetermineLDAPScheme(scheme string) (Scheme, error) {
	switch Scheme(scheme) {
	case SchemeLDAP, SchemeLDAPS:
		return Scheme(scheme), nil
	default:
		return "", fmt.Errorf("invalid scheme %q", scheme)
	}
}

// DetermineLDAPHost determines the host and port for the LDAP connection.
// The default host is localhost; the default port for scheme "ldap" is 389, for "ldaps" is 686
func DetermineLDAPHost(hostport string, scheme Scheme) (string, error) {
	if len(hostport) == 0 {
		hostport = defaultHost
	}
	// add port if missing
	if _, _, err := net.SplitHostPort(hostport); err != nil {
		switch scheme {
		case SchemeLDAPS:
			return net.JoinHostPort(hostport, strconv.Itoa(defaultLDAPSPort)), nil
		case SchemeLDAP:
			return net.JoinHostPort(hostport, strconv.Itoa(defaultLDAPPort)), nil
		default:
			return "", fmt.Errorf("no default port for scheme %q", scheme)
		}
	}
	// nothing needed to be done
	return hostport, nil
}

// SplitLDAPQuery splits the query in the URL into the substituent parts. All sections are optional.
// Query syntax is attribute?scope?filter?extensions
func SplitLDAPQuery(query string) (attributes, scope, filter, extensions string, err error) {
	parts := strings.Split(query, "?")
	switch len(parts) {
	case 4:
		extensions = parts[3]
		fallthrough
	case 3:
		if v, err := url.QueryUnescape(parts[2]); err != nil {
			return "", "", "", "", err
		} else {
			filter = v
		}
		fallthrough
	case 2:
		if v, err := url.QueryUnescape(parts[1]); err != nil {
			return "", "", "", "", err
		} else {
			scope = v
		}
		fallthrough
	case 1:
		if v, err := url.QueryUnescape(parts[0]); err != nil {
			return "", "", "", "", err
		} else {
			attributes = v
		}
		return attributes, scope, filter, extensions, nil
	case 0:
		return
	default:
		err = fmt.Errorf("too many query options %q", query)
		return "", "", "", "", err
	}
}

// DeterminmeLDAPScope determines the LDAP search scope. Scope is one of "sub", "one", or "base"
// Default to "sub" to match mod_auth_ldap
func DetermineLDAPScope(scope string) (Scope, error) {
	switch scope {
	case "", scopeWholeSubtreeString:
		return ScopeWholeSubtree, nil
	case scopeSingleLevelString:
		return ScopeSingleLevel, nil
	case scopeBaseObjectString:
		return ScopeBaseObject, nil
	default:
		return -1, fmt.Errorf("invalid scope %q", scope)
	}
}

// DetermineLDAPFilter determines the LDAP search filter. Filter is a valid LDAP filter
// Default to "(objectClass=*)" per RFC
func DetermineLDAPFilter(filter string) (string, error) {
	if len(filter) == 0 {
		return defaultFilter, nil
	}
	if _, err := ldap.CompileFilter(filter); err != nil {
		return "", fmt.Errorf("invalid filter: %v", err)
	}
	return filter, nil
}

func DetermineDerefAliasesBehavior(derefAliasesString string) (DerefAliases, error) {
	mapping := map[string]DerefAliases{
		"never":  DerefAliasesNever,
		"search": DerefAliasesSearching,
		"base":   DerefAliasesFinding,
		"always": DerefAliasesAlways,
	}
	derefAliases, exists := mapping[derefAliasesString]
	if !exists {
		return -1, fmt.Errorf("not a valid LDAP alias dereferncing behavior: %s", derefAliasesString)
	}
	return derefAliases, nil
}
<commit_msg>Use const values as string for defaultLDAP(S)Port<commit_after>package ldaputil

import (
	"fmt"
	"net"
	"net/url"
	"strings"

	"gopkg.in/ldap.v2"
)

// Scheme is a valid ldap scheme
type Scheme string

const (
	SchemeLDAP  Scheme = "ldap"
	SchemeLDAPS Scheme = "ldaps"
)

// Scope is a valid LDAP search scope
type Scope int

const (
	ScopeWholeSubtree Scope = ldap.ScopeWholeSubtree
	ScopeSingleLevel  Scope = ldap.ScopeSingleLevel
	ScopeBaseObject   Scope = ldap.ScopeBaseObject
)

// DerefAliases is a valid LDAP alias dereference parameter
type DerefAliases int

const (
	DerefAliasesNever     = ldap.NeverDerefAliases
	DerefAliasesSearching = ldap.DerefInSearching
	DerefAliasesFinding   = ldap.DerefFindingBaseObj
	DerefAliasesAlways    = ldap.DerefAlways
)

const (
	defaultLDAPPort  = "389"
	defaultLDAPSPort = "636"

	defaultHost           = "localhost"
	defaultQueryAttribute = "uid"
	defaultFilter         = "(objectClass=*)"

	scopeWholeSubtreeString = "sub"
	scopeSingleLevelString  = "one"
	scopeBaseObjectString   = "base"

	criticalExtensionPrefix = "!"
)

// LDAPURL holds a parsed RFC 2255 URL
type LDAPURL struct {
	// Scheme is ldap or ldaps
	Scheme Scheme
	// Host is the host:port of the LDAP server
	Host string
	// The DN of the branch of the directory where all searches should start from
	BaseDN string
	// The attribute to search for
	QueryAttribute string
	// The scope of the search. Can be ldap.ScopeWholeSubtree, ldap.ScopeSingleLevel, or ldap.ScopeBaseObject
	Scope Scope
	// A valid LDAP search filter (e.g. "(objectClass=*)")
	Filter string
}

// ParseURL parsed the given ldapURL as an RFC 2255 URL
// The syntax of the URL is ldap://host:port/basedn?attribute?scope?filter
func ParseURL(ldapURL string) (LDAPURL, error) {
	// Must be a valid URL to start
	parsedURL, err := url.Parse(ldapURL)
	if err != nil {
		return LDAPURL{}, err
	}

	opts := LDAPURL{}

	determinedScheme, err := DetermineLDAPScheme(parsedURL.Scheme)
	if err != nil {
		return LDAPURL{}, err
	}
	opts.Scheme = determinedScheme

	determinedHost, err := DetermineLDAPHost(parsedURL.Host, opts.Scheme)
	if err != nil {
		return LDAPURL{}, err
	}
	opts.Host = determinedHost

	// Set base dn (default to "")
	// url.Parse() already percent-decodes the path
	opts.BaseDN = strings.TrimLeft(parsedURL.Path, "/")

	attributes, scope, filter, extensions, err := SplitLDAPQuery(parsedURL.RawQuery)
	if err != nil {
		return LDAPURL{}, err
	}

	// Attributes contains comma-separated attributes
	// Set query attribute to first attribute
	// Default to uid to match mod_auth_ldap
	opts.QueryAttribute = strings.Split(attributes, ",")[0]
	if len(opts.QueryAttribute) == 0 {
		opts.QueryAttribute = defaultQueryAttribute
	}

	determinedScope, err := DetermineLDAPScope(scope)
	if err != nil {
		return LDAPURL{}, err
	}
	opts.Scope = determinedScope

	determinedFilter, err := DetermineLDAPFilter(filter)
	if err != nil {
		return LDAPURL{}, err
	}
	opts.Filter = determinedFilter

	// Extensions are in "name=value,name2=value2" form
	// Critical extensions are prefixed with a !
	// Optional extensions are ignored, per RFC
	// Fail if there are any critical extensions, since we don't support any
	if len(extensions) > 0 {
		for _, extension := range strings.Split(extensions, ",") {
			exttype := strings.SplitN(extension, "=", 2)[0]
			if strings.HasPrefix(exttype, criticalExtensionPrefix) {
				return LDAPURL{}, fmt.Errorf("unsupported critical extension %s", extension)
			}
		}
	}

	return opts, nil

}

// DetermineLDAPScheme determines the LDAP connection scheme. Scheme is one of "ldap" or "ldaps"
// Default to "ldap"
func DetermineLDAPScheme(scheme string) (Scheme, error) {
	switch Scheme(scheme) {
	case SchemeLDAP, SchemeLDAPS:
		return Scheme(scheme), nil
	default:
		return "", fmt.Errorf("invalid scheme %q", scheme)
	}
}

// DetermineLDAPHost determines the host and port for the LDAP connection.
// The default host is localhost; the default port for scheme "ldap" is 389, for "ldaps" is 686
func DetermineLDAPHost(hostport string, scheme Scheme) (string, error) {
	if len(hostport) == 0 {
		hostport = defaultHost
	}
	// add port if missing
	if _, _, err := net.SplitHostPort(hostport); err != nil {
		switch scheme {
		case SchemeLDAPS:
			return net.JoinHostPort(hostport, defaultLDAPSPort), nil
		case SchemeLDAP:
			return net.JoinHostPort(hostport, defaultLDAPPort), nil
		default:
			return "", fmt.Errorf("no default port for scheme %q", scheme)
		}
	}
	// nothing needed to be done
	return hostport, nil
}

// SplitLDAPQuery splits the query in the URL into the substituent parts. All sections are optional.
// Query syntax is attribute?scope?filter?extensions
func SplitLDAPQuery(query string) (attributes, scope, filter, extensions string, err error) {
	parts := strings.Split(query, "?")
	switch len(parts) {
	case 4:
		extensions = parts[3]
		fallthrough
	case 3:
		if v, err := url.QueryUnescape(parts[2]); err != nil {
			return "", "", "", "", err
		} else {
			filter = v
		}
		fallthrough
	case 2:
		if v, err := url.QueryUnescape(parts[1]); err != nil {
			return "", "", "", "", err
		} else {
			scope = v
		}
		fallthrough
	case 1:
		if v, err := url.QueryUnescape(parts[0]); err != nil {
			return "", "", "", "", err
		} else {
			attributes = v
		}
		return attributes, scope, filter, extensions, nil
	case 0:
		return
	default:
		err = fmt.Errorf("too many query options %q", query)
		return "", "", "", "", err
	}
}

// DeterminmeLDAPScope determines the LDAP search scope. Scope is one of "sub", "one", or "base"
// Default to "sub" to match mod_auth_ldap
func DetermineLDAPScope(scope string) (Scope, error) {
	switch scope {
	case "", scopeWholeSubtreeString:
		return ScopeWholeSubtree, nil
	case scopeSingleLevelString:
		return ScopeSingleLevel, nil
	case scopeBaseObjectString:
		return ScopeBaseObject, nil
	default:
		return -1, fmt.Errorf("invalid scope %q", scope)
	}
}

// DetermineLDAPFilter determines the LDAP search filter. Filter is a valid LDAP filter
// Default to "(objectClass=*)" per RFC
func DetermineLDAPFilter(filter string) (string, error) {
	if len(filter) == 0 {
		return defaultFilter, nil
	}
	if _, err := ldap.CompileFilter(filter); err != nil {
		return "", fmt.Errorf("invalid filter: %v", err)
	}
	return filter, nil
}

func DetermineDerefAliasesBehavior(derefAliasesString string) (DerefAliases, error) {
	mapping := map[string]DerefAliases{
		"never":  DerefAliasesNever,
		"search": DerefAliasesSearching,
		"base":   DerefAliasesFinding,
		"always": DerefAliasesAlways,
	}
	derefAliases, exists := mapping[derefAliasesString]
	if !exists {
		return -1, fmt.Errorf("not a valid LDAP alias dereferncing behavior: %s", derefAliasesString)
	}
	return derefAliases, nil
}
<|endoftext|>