mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-30 20:00:37 +00:00
b7ebaf6d20
* Update macaron * Various wiki bug fixes
390 lines
10 KiB
Go
390 lines
10 KiB
Go
// Copyright 2015 The Macaron Authors
|
|
//
|
|
// 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 macaron
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/Unknwon/com"
|
|
)
|
|
|
|
type patternType int8
|
|
|
|
const (
|
|
_PATTERN_STATIC patternType = iota // /home
|
|
_PATTERN_REGEXP // /:id([0-9]+)
|
|
_PATTERN_PATH_EXT // /*.*
|
|
_PATTERN_HOLDER // /:user
|
|
_PATTERN_MATCH_ALL // /*
|
|
)
|
|
|
|
// Leaf represents a leaf route information.
|
|
type Leaf struct {
|
|
parent *Tree
|
|
|
|
typ patternType
|
|
pattern string
|
|
rawPattern string // Contains wildcard instead of regexp
|
|
wildcards []string
|
|
reg *regexp.Regexp
|
|
optional bool
|
|
|
|
handle Handle
|
|
}
|
|
|
|
var wildcardPattern = regexp.MustCompile(`:[a-zA-Z0-9]+`)
|
|
|
|
func isSpecialRegexp(pattern, regStr string, pos []int) bool {
|
|
return len(pattern) >= pos[1]+len(regStr) && pattern[pos[1]:pos[1]+len(regStr)] == regStr
|
|
}
|
|
|
|
// getNextWildcard tries to find next wildcard and update pattern with corresponding regexp.
|
|
func getNextWildcard(pattern string) (wildcard, _ string) {
|
|
pos := wildcardPattern.FindStringIndex(pattern)
|
|
if pos == nil {
|
|
return "", pattern
|
|
}
|
|
wildcard = pattern[pos[0]:pos[1]]
|
|
|
|
// Reach last character or no regexp is given.
|
|
if len(pattern) == pos[1] {
|
|
return wildcard, strings.Replace(pattern, wildcard, `(.+)`, 1)
|
|
} else if pattern[pos[1]] != '(' {
|
|
switch {
|
|
case isSpecialRegexp(pattern, ":int", pos):
|
|
pattern = strings.Replace(pattern, ":int", "([0-9]+)", 1)
|
|
case isSpecialRegexp(pattern, ":string", pos):
|
|
pattern = strings.Replace(pattern, ":string", "([\\w]+)", 1)
|
|
default:
|
|
return wildcard, strings.Replace(pattern, wildcard, `(.+)`, 1)
|
|
}
|
|
}
|
|
|
|
// Cut out placeholder directly.
|
|
return wildcard, pattern[:pos[0]] + pattern[pos[1]:]
|
|
}
|
|
|
|
func getWildcards(pattern string) (string, []string) {
|
|
wildcards := make([]string, 0, 2)
|
|
|
|
// Keep getting next wildcard until nothing is left.
|
|
var wildcard string
|
|
for {
|
|
wildcard, pattern = getNextWildcard(pattern)
|
|
if len(wildcard) > 0 {
|
|
wildcards = append(wildcards, wildcard)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return pattern, wildcards
|
|
}
|
|
|
|
// getRawPattern removes all regexp but keeps wildcards for building URL path.
|
|
func getRawPattern(rawPattern string) string {
|
|
rawPattern = strings.Replace(rawPattern, ":int", "", -1)
|
|
rawPattern = strings.Replace(rawPattern, ":string", "", -1)
|
|
|
|
for {
|
|
startIdx := strings.Index(rawPattern, "(")
|
|
if startIdx == -1 {
|
|
break
|
|
}
|
|
|
|
closeIdx := strings.Index(rawPattern, ")")
|
|
if closeIdx > -1 {
|
|
rawPattern = rawPattern[:startIdx] + rawPattern[closeIdx+1:]
|
|
}
|
|
}
|
|
return rawPattern
|
|
}
|
|
|
|
func checkPattern(pattern string) (typ patternType, rawPattern string, wildcards []string, reg *regexp.Regexp) {
|
|
pattern = strings.TrimLeft(pattern, "?")
|
|
rawPattern = getRawPattern(pattern)
|
|
|
|
if pattern == "*" {
|
|
typ = _PATTERN_MATCH_ALL
|
|
} else if pattern == "*.*" {
|
|
typ = _PATTERN_PATH_EXT
|
|
} else if strings.Contains(pattern, ":") {
|
|
typ = _PATTERN_REGEXP
|
|
pattern, wildcards = getWildcards(pattern)
|
|
if pattern == "(.+)" {
|
|
typ = _PATTERN_HOLDER
|
|
} else {
|
|
reg = regexp.MustCompile(pattern)
|
|
}
|
|
}
|
|
return typ, rawPattern, wildcards, reg
|
|
}
|
|
|
|
func NewLeaf(parent *Tree, pattern string, handle Handle) *Leaf {
|
|
typ, rawPattern, wildcards, reg := checkPattern(pattern)
|
|
optional := false
|
|
if len(pattern) > 0 && pattern[0] == '?' {
|
|
optional = true
|
|
}
|
|
return &Leaf{parent, typ, pattern, rawPattern, wildcards, reg, optional, handle}
|
|
}
|
|
|
|
// URLPath build path part of URL by given pair values.
|
|
func (l *Leaf) URLPath(pairs ...string) string {
|
|
if len(pairs)%2 != 0 {
|
|
panic("number of pairs does not match")
|
|
}
|
|
|
|
urlPath := l.rawPattern
|
|
parent := l.parent
|
|
for parent != nil {
|
|
urlPath = parent.rawPattern + "/" + urlPath
|
|
parent = parent.parent
|
|
}
|
|
for i := 0; i < len(pairs); i += 2 {
|
|
if len(pairs[i]) == 0 {
|
|
panic("pair value cannot be empty: " + com.ToStr(i))
|
|
} else if pairs[i][0] != ':' && pairs[i] != "*" && pairs[i] != "*.*" {
|
|
pairs[i] = ":" + pairs[i]
|
|
}
|
|
urlPath = strings.Replace(urlPath, pairs[i], pairs[i+1], 1)
|
|
}
|
|
return urlPath
|
|
}
|
|
|
|
// Tree represents a router tree in Macaron.
|
|
type Tree struct {
|
|
parent *Tree
|
|
|
|
typ patternType
|
|
pattern string
|
|
rawPattern string
|
|
wildcards []string
|
|
reg *regexp.Regexp
|
|
|
|
subtrees []*Tree
|
|
leaves []*Leaf
|
|
}
|
|
|
|
func NewSubtree(parent *Tree, pattern string) *Tree {
|
|
typ, rawPattern, wildcards, reg := checkPattern(pattern)
|
|
return &Tree{parent, typ, pattern, rawPattern, wildcards, reg, make([]*Tree, 0, 5), make([]*Leaf, 0, 5)}
|
|
}
|
|
|
|
func NewTree() *Tree {
|
|
return NewSubtree(nil, "")
|
|
}
|
|
|
|
func (t *Tree) addLeaf(pattern string, handle Handle) *Leaf {
|
|
for i := 0; i < len(t.leaves); i++ {
|
|
if t.leaves[i].pattern == pattern {
|
|
return t.leaves[i]
|
|
}
|
|
}
|
|
|
|
leaf := NewLeaf(t, pattern, handle)
|
|
|
|
// Add exact same leaf to grandparent/parent level without optional.
|
|
if leaf.optional {
|
|
parent := leaf.parent
|
|
if parent.parent != nil {
|
|
parent.parent.addLeaf(parent.pattern, handle)
|
|
} else {
|
|
parent.addLeaf("", handle) // Root tree can add as empty pattern.
|
|
}
|
|
}
|
|
|
|
i := 0
|
|
for ; i < len(t.leaves); i++ {
|
|
if leaf.typ < t.leaves[i].typ {
|
|
break
|
|
}
|
|
}
|
|
|
|
if i == len(t.leaves) {
|
|
t.leaves = append(t.leaves, leaf)
|
|
} else {
|
|
t.leaves = append(t.leaves[:i], append([]*Leaf{leaf}, t.leaves[i:]...)...)
|
|
}
|
|
return leaf
|
|
}
|
|
|
|
func (t *Tree) addSubtree(segment, pattern string, handle Handle) *Leaf {
|
|
for i := 0; i < len(t.subtrees); i++ {
|
|
if t.subtrees[i].pattern == segment {
|
|
return t.subtrees[i].addNextSegment(pattern, handle)
|
|
}
|
|
}
|
|
|
|
subtree := NewSubtree(t, segment)
|
|
i := 0
|
|
for ; i < len(t.subtrees); i++ {
|
|
if subtree.typ < t.subtrees[i].typ {
|
|
break
|
|
}
|
|
}
|
|
|
|
if i == len(t.subtrees) {
|
|
t.subtrees = append(t.subtrees, subtree)
|
|
} else {
|
|
t.subtrees = append(t.subtrees[:i], append([]*Tree{subtree}, t.subtrees[i:]...)...)
|
|
}
|
|
return subtree.addNextSegment(pattern, handle)
|
|
}
|
|
|
|
func (t *Tree) addNextSegment(pattern string, handle Handle) *Leaf {
|
|
pattern = strings.TrimPrefix(pattern, "/")
|
|
|
|
i := strings.Index(pattern, "/")
|
|
if i == -1 {
|
|
return t.addLeaf(pattern, handle)
|
|
}
|
|
return t.addSubtree(pattern[:i], pattern[i+1:], handle)
|
|
}
|
|
|
|
func (t *Tree) Add(pattern string, handle Handle) *Leaf {
|
|
pattern = strings.TrimSuffix(pattern, "/")
|
|
return t.addNextSegment(pattern, handle)
|
|
}
|
|
|
|
func (t *Tree) matchLeaf(globLevel int, url string, params Params) (Handle, bool) {
|
|
url, err := PathUnescape(url)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
for i := 0; i < len(t.leaves); i++ {
|
|
switch t.leaves[i].typ {
|
|
case _PATTERN_STATIC:
|
|
if t.leaves[i].pattern == url {
|
|
return t.leaves[i].handle, true
|
|
}
|
|
case _PATTERN_REGEXP:
|
|
results := t.leaves[i].reg.FindStringSubmatch(url)
|
|
// Number of results and wildcasrd should be exact same.
|
|
if len(results)-1 != len(t.leaves[i].wildcards) {
|
|
break
|
|
}
|
|
|
|
for j := 0; j < len(t.leaves[i].wildcards); j++ {
|
|
params[t.leaves[i].wildcards[j]] = results[j+1]
|
|
}
|
|
return t.leaves[i].handle, true
|
|
case _PATTERN_PATH_EXT:
|
|
j := strings.LastIndex(url, ".")
|
|
if j > -1 {
|
|
params[":path"] = url[:j]
|
|
params[":ext"] = url[j+1:]
|
|
} else {
|
|
params[":path"] = url
|
|
}
|
|
return t.leaves[i].handle, true
|
|
case _PATTERN_HOLDER:
|
|
params[t.leaves[i].wildcards[0]] = url
|
|
return t.leaves[i].handle, true
|
|
case _PATTERN_MATCH_ALL:
|
|
params["*"] = url
|
|
params["*"+com.ToStr(globLevel)] = url
|
|
return t.leaves[i].handle, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (t *Tree) matchSubtree(globLevel int, segment, url string, params Params) (Handle, bool) {
|
|
unescapedSegment, err := PathUnescape(segment)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
for i := 0; i < len(t.subtrees); i++ {
|
|
switch t.subtrees[i].typ {
|
|
case _PATTERN_STATIC:
|
|
if t.subtrees[i].pattern == unescapedSegment {
|
|
if handle, ok := t.subtrees[i].matchNextSegment(globLevel, url, params); ok {
|
|
return handle, true
|
|
}
|
|
}
|
|
case _PATTERN_REGEXP:
|
|
results := t.subtrees[i].reg.FindStringSubmatch(unescapedSegment)
|
|
if len(results)-1 != len(t.subtrees[i].wildcards) {
|
|
break
|
|
}
|
|
|
|
for j := 0; j < len(t.subtrees[i].wildcards); j++ {
|
|
params[t.subtrees[i].wildcards[j]] = results[j+1]
|
|
}
|
|
if handle, ok := t.subtrees[i].matchNextSegment(globLevel, url, params); ok {
|
|
return handle, true
|
|
}
|
|
case _PATTERN_HOLDER:
|
|
if handle, ok := t.subtrees[i].matchNextSegment(globLevel+1, url, params); ok {
|
|
params[t.subtrees[i].wildcards[0]] = unescapedSegment
|
|
return handle, true
|
|
}
|
|
case _PATTERN_MATCH_ALL:
|
|
if handle, ok := t.subtrees[i].matchNextSegment(globLevel+1, url, params); ok {
|
|
params["*"+com.ToStr(globLevel)] = unescapedSegment
|
|
return handle, true
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(t.leaves) > 0 {
|
|
leaf := t.leaves[len(t.leaves)-1]
|
|
unescapedURL, err := PathUnescape(segment + "/" + url)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
if leaf.typ == _PATTERN_PATH_EXT {
|
|
j := strings.LastIndex(unescapedURL, ".")
|
|
if j > -1 {
|
|
params[":path"] = unescapedURL[:j]
|
|
params[":ext"] = unescapedURL[j+1:]
|
|
} else {
|
|
params[":path"] = unescapedURL
|
|
}
|
|
return leaf.handle, true
|
|
} else if leaf.typ == _PATTERN_MATCH_ALL {
|
|
params["*"] = unescapedURL
|
|
params["*"+com.ToStr(globLevel)] = unescapedURL
|
|
return leaf.handle, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (t *Tree) matchNextSegment(globLevel int, url string, params Params) (Handle, bool) {
|
|
i := strings.Index(url, "/")
|
|
if i == -1 {
|
|
return t.matchLeaf(globLevel, url, params)
|
|
}
|
|
return t.matchSubtree(globLevel, url[:i], url[i+1:], params)
|
|
}
|
|
|
|
func (t *Tree) Match(url string) (Handle, Params, bool) {
|
|
url = strings.TrimPrefix(url, "/")
|
|
url = strings.TrimSuffix(url, "/")
|
|
params := make(Params)
|
|
handle, ok := t.matchNextSegment(0, url, params)
|
|
return handle, params, ok
|
|
}
|
|
|
|
// MatchTest returns true if given URL is matched by given pattern.
|
|
func MatchTest(pattern, url string) bool {
|
|
t := NewTree()
|
|
t.Add(pattern, nil)
|
|
_, _, ok := t.Match(url)
|
|
return ok
|
|
}
|