yao/sui/api/request.go
2024-02-20 20:07:26 +08:00

354 lines
8.2 KiB
Go

package api
import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/application"
"github.com/yaoapp/kun/exception"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/sui/core"
)
// Request is the request for the page API.
type Request struct {
File string
*core.Request
context *gin.Context
}
// NewRequestContext is the constructor for Request.
func NewRequestContext(c *gin.Context) (*Request, int, error) {
file, params, err := parserPath(c)
if err != nil {
return nil, 404, err
}
payload, body, err := payload(c)
if err != nil {
return nil, 500, err
}
schema := c.Request.URL.Scheme
if schema == "" {
schema = "http"
}
domain := c.Request.URL.Hostname()
if domain == "" {
domain = strings.Split(c.Request.Host, ":")[0]
}
path := strings.TrimSuffix(c.Request.URL.Path, ".sui")
return &Request{
File: file,
context: c,
Request: &core.Request{
Method: c.Request.Method,
Query: c.Request.URL.Query(),
Body: body,
Payload: payload,
Referer: c.Request.Referer(),
Headers: url.Values(c.Request.Header),
Params: params,
URL: core.ReqeustURL{
URL: fmt.Sprintf("%s://%s%s", schema, c.Request.Host, path),
Host: c.Request.Host,
Path: path,
Domain: domain,
Scheme: schema,
},
},
}, 200, nil
}
// Render is the response for the page API.
func (r *Request) Render() (string, int, error) {
c := core.GetCache(r.File)
c = nil // disable cache @todo disable cache on development
if c == nil {
// Read the file
content, err := application.App.Read(r.File)
if err != nil {
return "", 404, err
}
doc, err := core.NewDocument(content)
if err != nil {
return "", 500, err
}
guard := ""
guardRedirect := ""
configText := ""
configSel := doc.Find("script[name=config]")
if configSel != nil && configSel.Length() > 0 {
configText = configSel.Text()
configSel.Remove()
var conf core.PageConfig
err := jsoniter.UnmarshalFromString(configText, &conf)
if err != nil {
return "", 500, fmt.Errorf("config error, please re-complie the page %s", err.Error())
}
// Redirect the page (should refector before release)
// guard=cookie-jwt:redirect-url redirect to the url if not authorized
// guard=cookie-jwt return {code: 403, message: "Not Authorized"}
guard = conf.Guard
if strings.Contains(conf.Guard, ":") {
parts := strings.Split(conf.Guard, ":")
guard = parts[0]
guardRedirect = parts[1]
}
}
dataText := ""
dataSel := doc.Find("script[name=data]")
if dataSel != nil && dataSel.Length() > 0 {
dataText = dataSel.Text()
dataSel.Remove()
}
globalDataText := ""
globalDataSel := doc.Find("script[name=global]")
if globalDataSel != nil && globalDataSel.Length() > 0 {
globalDataText = globalDataSel.Text()
globalDataSel.Remove()
}
html, err := doc.Html()
if err != nil {
return "", 500, fmt.Errorf("parse error, please re-complie the page %s", err.Error())
}
// Save to The Cache
// c = core.SetCache(r.File, html, dataText, globalDataText)
c = &core.Cache{
Data: dataText,
Global: globalDataText,
HTML: html,
Guard: guard,
GuardRedirect: guardRedirect,
Config: configText,
}
log.Trace("The page %s is cached", r.File)
}
// Guard the page
if c.Guard != "" && r.context != nil {
if guard, has := Guards[c.Guard]; has {
err := guard(r)
if err != nil {
// Redirect the page (should refector before release)
if c.GuardRedirect != "" {
redirect := c.GuardRedirect
data := core.Data{}
if c.Data != "" {
data, err = r.Request.ExecString(c.Data)
if err != nil {
return "", 500, fmt.Errorf("data error, please re-complie the page %s", err.Error())
}
}
if c.Global != "" {
global, err := r.Request.ExecString(c.Global)
if err != nil {
return "", 500, fmt.Errorf("global data error, please re-complie the page %s", err.Error())
}
data["$global"] = global
}
redirect, _ = data.Replace(redirect)
return "", 302, fmt.Errorf("%s", redirect)
}
// Return the error
ex := exception.Err(err, 403)
return "", ex.Code, fmt.Errorf("%s", ex.Message)
}
} else {
// Process the guard
err := r.processGuard(c.Guard)
if err != nil {
ex := exception.Err(err, 403)
return "", ex.Code, fmt.Errorf("%s", ex.Message)
}
}
}
var err error
data := core.Data{}
if c.Data != "" {
data, err = r.Request.ExecString(c.Data)
if err != nil {
return "", 500, fmt.Errorf("data error, please re-complie the page %s", err.Error())
}
}
if c.Global != "" {
global, err := r.Request.ExecString(c.Global)
if err != nil {
return "", 500, fmt.Errorf("global data error, please re-complie the page %s", err.Error())
}
data["$global"] = global
}
// Set the page request data
data["$payload"] = r.Request.Payload
data["$query"] = r.Request.Query
data["$param"] = r.Request.Params
data["$url"] = r.Request.URL
printData := false
if r.Query != nil && r.Query.Has("__sui_print_data") {
printData = true
}
parser := core.NewTemplateParser(data, &core.ParserOption{PrintData: printData, Request: true})
html, err := parser.Render(c.HTML)
if err != nil {
return "", 500, fmt.Errorf("render error, please re-complie the page %s", err.Error())
}
return html, 200, nil
}
func parserPath(c *gin.Context) (string, map[string]string, error) {
params := map[string]string{}
parts := strings.Split(strings.TrimSuffix(c.Request.URL.Path, ".sui"), "/")[1:]
if len(parts) < 1 {
return "", nil, fmt.Errorf("path parts error: %s", strings.Join(parts, "/"))
}
fileParts := []string{string(os.PathSeparator), "public"}
// Match the sui
matchers := core.RouteExactMatchers[parts[0]]
if matchers == nil {
for matcher, reMatchers := range core.RouteMatchers {
matched := matcher.FindStringSubmatch(parts[0])
if len(matched) > 0 {
matchers = reMatchers
fileParts = append(fileParts, matched[0])
break
}
}
}
// No matchers
if matchers == nil {
if len(parts) < 1 {
return "", nil, fmt.Errorf("path parts error: %s", strings.Join(parts, "/"))
}
fileParts = append(fileParts, parts...)
return filepath.Join(fileParts...) + ".sui", params, nil
}
// Match the page parts
for i, part := range parts[1:] {
if len(matchers) < i+1 {
return "", nil, fmt.Errorf("matchers length error %d < %d", len(matchers), i+1)
}
parent := ""
if i > 0 {
parent = parts[i]
}
matched := false
for _, matcher := range matchers[i] {
// Filter the parent
if matcher.Parent != "" && matcher.Parent != parent {
continue
}
if matcher.Exact == part {
fileParts = append(fileParts, matcher.Exact)
matched = true
break
} else if matcher.Regex != nil {
if matcher.Regex.MatchString(part) {
file := matcher.Ref
key := strings.TrimRight(strings.TrimLeft(file, "["), "]")
params[key] = part
fileParts = append(fileParts, file)
matched = true
break
}
}
}
if !matched {
return "", nil, fmt.Errorf("route does not match")
}
}
return filepath.Join(fileParts...) + ".sui", params, nil
}
func params(c *gin.Context) map[string]string {
return nil
}
func payload(c *gin.Context) (map[string]interface{}, interface{}, error) {
contentType := c.Request.Header.Get("Content-Type")
var payload map[string]interface{}
var body interface{}
switch contentType {
case "application/x-www-form-urlencoded":
c.Request.ParseForm()
payload = make(map[string]interface{})
for key, value := range c.Request.Form {
payload[key] = value
}
body = nil
break
case "multipart/form-data":
c.Request.ParseMultipartForm(32 << 20)
payload = make(map[string]interface{})
for key, value := range c.Request.MultipartForm.Value {
payload[key] = value
}
body = nil
break
case "application/json":
if c.Request.Body == nil {
return nil, nil, nil
}
c.Bind(&payload)
body = nil
break
default:
if c.Request.Body == nil {
return nil, nil, nil
}
var data []byte
_, err := c.Request.Body.Read(data)
if err != nil && err.Error() != "EOF" {
return nil, nil, err
}
body = data
}
return payload, body, nil
}