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 }