github.com/gocaveman/caveman@v0.0.0-20191211162744-0ddf99dbdf6e/regions/regions.go (about)

     1  package regions
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"log"
    10  	"math"
    11  	"net/http"
    12  	"path"
    13  	"regexp"
    14  	"sort"
    15  	"strings"
    16  	"text/template"
    17  
    18  	"github.com/gocaveman/caveman/webutil"
    19  )
    20  
    21  var ErrWriteNotSupported = errors.New("write not supported")
    22  
    23  // BlockDefiner lets us define blocks.  Matches renderer.BlockDefiner
    24  type BlockDefiner interface {
    25  	BlockDefine(name, content string, condition func(ctx context.Context) (bool, error))
    26  }
    27  
    28  // RegionHandler analyzes the regions against the current request and uses BlockDefiner (see renderer package) to create the appropriate blocks.
    29  type RegionHandler struct {
    30  	Store Store `autowire:""`
    31  }
    32  
    33  func (h *RegionHandler) ServeHTTPChain(w http.ResponseWriter, r *http.Request) (http.ResponseWriter, *http.Request) {
    34  	if h.Store == nil {
    35  		panic("RegionHandler.Store is nil")
    36  	}
    37  
    38  	ctx := r.Context()
    39  	blockDefiner, ok := ctx.Value("renderer.BlockDefiner").(BlockDefiner)
    40  	if !ok {
    41  		// TODO: not sure what the rule is on this - should we fail, just log, or what;
    42  		// log for now...
    43  		log.Printf("RegionHandler: `renderer.BlockDefiner` is not set on context, cannot define blocks.  Are you missing the renderer.BlockDefineHandler in your handler list?")
    44  		return w, r
    45  	}
    46  
    47  	defs, err := h.Store.AllDefinitions()
    48  	if err != nil {
    49  		webutil.HTTPError(w, r, err, "error getting region definitions", 500)
    50  		return w, r
    51  	}
    52  
    53  	// should be handled now...
    54  	// log.Printf("FIXME: we really should be doing this check at a time when both the template and pageinfo meta are available, so those can be used in the condition... which would mean maybe BlockDefiner needs to allow for a condition or something...")
    55  
    56  	regionNames := defs.RegionNames()
    57  	for _, regionName := range regionNames {
    58  		// FIXME: what if regionName is empty or doesn't start with plus - need to think
    59  		// the error conditions through and do a better job.
    60  		regionDefs := defs.ForRegionName(regionName).SortedCopy()
    61  		for i := range regionDefs {
    62  
    63  			def := regionDefs[i]
    64  
    65  			if def.Disabled {
    66  				continue
    67  			}
    68  
    69  			// log.Printf("FIXME: so this can't work like this we need to be able to delay the check using the BlockDefiner condition stuff, which means context-only")
    70  			// if !def.MatchesRequest(r) {
    71  			// 	continue
    72  			// }
    73  
    74  			// name formatted so plusser adds to correct region, sorts in sequence and is unique
    75  			name := fmt.Sprintf("%s0%04d_%s", regionName, i, def.DefinitionID)
    76  
    77  			// use default context or if ContextMeta then include call for that
    78  			ctxStr := `$`
    79  			if len(def.ContextMeta) > 0 {
    80  				b, err := json.Marshal(def.ContextMeta)
    81  				if err != nil {
    82  					webutil.HTTPError(w, r, err, fmt.Sprintf("error marshaling JSON data for region defintion %q", def.DefinitionID), 500)
    83  					return w, r
    84  				}
    85  				ctxStr = fmt.Sprintf(`(WithValueMapJSON $ %q)`, b)
    86  			}
    87  
    88  			// condition check is delayed until later when everything possible is available on the context
    89  			// FIXME: condition should have error return
    90  			condition := func(ctx context.Context) (bool, error) {
    91  				return def.MatchesContext(ctx)
    92  			}
    93  
    94  			content := fmt.Sprintf(`{{template %q (WithValue %s "regions.DefinitionID" %q)}}`, def.TemplateName, ctxStr, def.DefinitionID)
    95  			// log.Printf("Doing BlockDefine(%q,...)\n%s", name, content)
    96  			blockDefiner.BlockDefine(name, content, condition)
    97  
    98  		}
    99  	}
   100  
   101  	return w, r
   102  }
   103  
   104  type Store interface {
   105  	WriteDefinition(d Definition) error
   106  	DeleteDefinition(defintionID string) error
   107  	AllDefinitions() (DefinitionList, error)
   108  }
   109  
   110  // Definition describes a single entry in a region.
   111  type Definition struct {
   112  	DefinitionID string  `json:"definition_id" yaml:"definition_id" db:"definition_id"` // must be globally unique, recommend prefixing with package name for built-in ones
   113  	RegionName   string  `json:"region_name" yaml:"region_name" db:"region_name"`       // name of the region this gets appended to, must end with a "+" to match template region names (see renderer/plusser.go)
   114  	Sequence     float64 `json:"sequence" yaml:"sequence" db:"sequence"`                // order of this in the region, by convention this is between 0 and 100 but can be any number supported by float64
   115  	Disabled     bool    `json:"disabled" yaml:"disabled" db:"disabled"`                // mainly here so we can override a definition as disabled
   116  	TemplateName string  `json:"template_name" yaml:"template_name" db:"template_name"` // name/path of template to include
   117  
   118  	CondIncludePaths string `json:"cond_include_paths" yaml:"cond_include_paths" db:"cond_include_paths"` // http request paths to include (wildcard pattern if starts with "/" or regexp, multiple are whitespace separated)
   119  	CondExcludePaths string `json:"cond_exclude_paths" yaml:"cond_exclude_paths" db:"cond_exclude_paths"` // http request paths to exclude (wildcard pattern if starts with "/" or regexp, multiple are whitespace separated)
   120  	CondTemplate     string `json:"cond_template" yaml:"cond_template" db:"cond_template"`                // custom condition as text/template, if first non-whitespace character is other than '0' means true, otherwise false, take precedence over other conditions
   121  
   122  	ContextMeta webutil.SimpleStringDataMap `json:"context_meta" yaml:"context_meta" db:"context_meta"` // static data that gets set on the context during the call
   123  }
   124  
   125  func (d *Definition) IsValid() error {
   126  
   127  	if d.DefinitionID == "" {
   128  		return fmt.Errorf("DefinitionID is empty")
   129  	}
   130  	if d.RegionName == "" {
   131  		return fmt.Errorf("RegionName is empty")
   132  	}
   133  	if math.IsNaN(d.Sequence) || math.IsInf(d.Sequence, 0) {
   134  		return fmt.Errorf("Sequence number is not valid")
   135  	}
   136  	if d.TemplateName == "" {
   137  		return fmt.Errorf("TemplateName is empty")
   138  	}
   139  	if _, err := condPaths(d.CondIncludePaths).checkPath("/"); err != nil {
   140  		return fmt.Errorf("CondIncludePaths error: %v", err)
   141  	}
   142  	if _, err := condPaths(d.CondExcludePaths).checkPath("/"); err != nil {
   143  		return fmt.Errorf("CondExcludePaths error: %v", err)
   144  	}
   145  
   146  	// FIXME: need to extract out the thing that deals with CondTemplate and at least
   147  	// parse it here
   148  
   149  	return nil
   150  }
   151  
   152  type condPaths string
   153  
   154  func (ps condPaths) checkPath(p string) (bool, error) {
   155  
   156  	p = path.Clean("/" + p)
   157  
   158  	parts := strings.Fields(string(ps))
   159  
   160  	found := false
   161  
   162  	for _, part := range parts {
   163  		if part == "" {
   164  			continue
   165  		}
   166  
   167  		// wildcard
   168  		if part[0] == '/' {
   169  
   170  			expr := `^` + strings.Replace(regexp.QuoteMeta(part), `\*`, `.*`, -1) + `$`
   171  			re, err := regexp.Compile(expr)
   172  			if err != nil {
   173  				return false, fmt.Errorf("checkPath wildcard regexp compile error (`%s`): %v", expr, err)
   174  			}
   175  
   176  			if re.MatchString(p) {
   177  				found = true
   178  				break
   179  			}
   180  
   181  		} else { // otherwise regexp
   182  
   183  			re, err := regexp.Compile(part)
   184  			if err != nil {
   185  				return false, fmt.Errorf("checkPath direct regexp compile error (`%s`): %v", part, err)
   186  			}
   187  
   188  			if re.MatchString(p) {
   189  				found = true
   190  				break
   191  			}
   192  
   193  		}
   194  
   195  	}
   196  
   197  	return found, nil
   198  
   199  }
   200  
   201  // MatchesContext checks the condition against the HTTP request in a context.
   202  func (d *Definition) MatchesContext(ctx context.Context) (bool, error) {
   203  
   204  	r, ok := ctx.Value("http.Request").(*http.Request)
   205  	if !ok {
   206  		// log.Printf("NO REQUEST")
   207  		return false, nil
   208  	}
   209  
   210  	// check CondTemplate only if set
   211  	if d.CondTemplate != "" {
   212  
   213  		t, err := template.New("_").Parse(d.CondTemplate)
   214  		if err != nil {
   215  			return false, fmt.Errorf("MatchesContext error while parsing condition template: %v\nTEMPLATE: %s", err, d.CondTemplate)
   216  		}
   217  
   218  		var buf bytes.Buffer
   219  		err = t.Execute(&buf, ctx)
   220  		if err != nil {
   221  			return false, fmt.Errorf("MatchesContext error while executing condition template: %v\nTEMPLATE: %s", err, d.CondTemplate)
   222  		}
   223  
   224  		bufStr := strings.TrimSpace(buf.String())
   225  		return len(bufStr) > 0 && bufStr[0] != '0', nil
   226  
   227  	}
   228  
   229  	var err error
   230  
   231  	// do include/exclude thing
   232  	ret := true
   233  	if strings.TrimSpace(d.CondIncludePaths) != "" {
   234  		// if any include condition then require one include to match
   235  		ret, err = condPaths(d.CondIncludePaths).checkPath(r.URL.Path)
   236  		if err != nil {
   237  			return false, err
   238  		}
   239  	}
   240  	// if exclude then veto prior state
   241  	v, err := condPaths(d.CondExcludePaths).checkPath(r.URL.Path)
   242  	if err != nil {
   243  		return false, err
   244  	}
   245  	if v {
   246  		ret = false
   247  	}
   248  
   249  	return ret, nil
   250  
   251  }
   252  
   253  type DefinitionList []Definition
   254  
   255  func (p DefinitionList) Len() int           { return len(p) }
   256  func (p DefinitionList) Less(i, j int) bool { return p[i].Sequence < p[j].Sequence }
   257  func (p DefinitionList) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
   258  
   259  // RegionNames returns a deduplicated slice of the RegionName fields of this list.
   260  func (dl DefinitionList) RegionNames() []string {
   261  	ret := make([]string, 0, 32)
   262  	retMap := make(map[string]bool, 32)
   263  	for _, d := range dl {
   264  		if !retMap[d.RegionName] {
   265  			retMap[d.RegionName] = true
   266  			ret = append(ret, d.RegionName)
   267  		}
   268  	}
   269  	sort.Strings(ret)
   270  	return ret
   271  }
   272  
   273  // ForRegionName returns the definitions that match a region name.
   274  func (dl DefinitionList) ForRegionName(regionName string) DefinitionList {
   275  	var ret DefinitionList
   276  	for _, d := range dl {
   277  		if d.RegionName == regionName {
   278  			ret = append(ret, d)
   279  		}
   280  	}
   281  	return ret
   282  }
   283  
   284  // SortedCopy returns a copy that is sorted by Sequence.
   285  func (dl DefinitionList) SortedCopy() DefinitionList {
   286  	ret := make(DefinitionList, len(dl))
   287  	copy(ret, dl)
   288  	sort.Sort(ret)
   289  	return ret
   290  }
   291  
   292  // ListStore implements Store on top of a slice of Defintions.  Read-only.
   293  // Makes it simple to implement a static Store in a theme or other Go code.
   294  type ListStore []Definition
   295  
   296  func (s ListStore) WriteDefinition(d Definition) error {
   297  	return ErrWriteNotSupported
   298  }
   299  func (s ListStore) DeleteDefinition(defintionID string) error {
   300  	return ErrWriteNotSupported
   301  }
   302  func (s ListStore) AllDefinitions() (DefinitionList, error) {
   303  	return DefinitionList(s), nil
   304  }
   305  
   306  // StackedStore implements Store on top of a slice of other Stores.
   307  // WriteDefinition and DeleteDefinition call each member of the slice in sequence
   308  // until one returns nil or an error other than ErrWriteNotSupported.
   309  // AllDefinitions returns a combined list of defintions from all stores
   310  // with the Stores earlier in the list taking precedence, e.g. a Definition
   311  // with a specific DefinitionID at Store index 0 will be returns instead of
   312  // a Definition with the same ID at Store index 1.
   313  type StackedStore []Store
   314  
   315  func (ss StackedStore) WriteDefinition(d Definition) error {
   316  	for _, s := range ss {
   317  		err := s.WriteDefinition(d)
   318  		if err == ErrWriteNotSupported {
   319  			continue
   320  		}
   321  		return err
   322  	}
   323  	return ErrWriteNotSupported
   324  }
   325  
   326  func (ss StackedStore) DeleteDefinition(defintionID string) error {
   327  	for _, s := range ss {
   328  		err := s.DeleteDefinition(defintionID)
   329  		if err == ErrWriteNotSupported {
   330  			continue
   331  		}
   332  		return err
   333  	}
   334  	return ErrWriteNotSupported
   335  }
   336  
   337  func (ss StackedStore) AllDefinitions() (DefinitionList, error) {
   338  	var ret DefinitionList
   339  	retMap := make(map[string]bool)
   340  	for _, s := range ss {
   341  		defs, err := s.AllDefinitions()
   342  		if err != nil {
   343  			return nil, err
   344  		}
   345  		for _, def := range defs {
   346  			if !retMap[def.DefinitionID] {
   347  				ret = append(ret, def)
   348  				retMap[def.DefinitionID] = true
   349  			}
   350  		}
   351  	}
   352  	return ret, nil
   353  }