199 строки
4.0 KiB
Go
199 строки
4.0 KiB
Go
package fragments
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/valyala/fasthttp"
|
|
"golang.org/x/net/html"
|
|
)
|
|
|
|
// Fragment is a <fragment> in the <header> or <body>
|
|
// of a HTML page.
|
|
type Fragment struct {
|
|
deferred bool
|
|
fallback string
|
|
method string
|
|
primary bool
|
|
src string
|
|
timeout int64
|
|
ref string
|
|
id string
|
|
|
|
statusCode int
|
|
head []*html.Node
|
|
|
|
s *goquery.Selection
|
|
}
|
|
|
|
// FromSelection creates a new fragment from a
|
|
// fragment selection in the DOM.
|
|
func FromSelection(s *goquery.Selection) *Fragment {
|
|
f := new(Fragment)
|
|
f.s = s
|
|
|
|
src, _ := s.Attr("src")
|
|
f.src = src
|
|
|
|
fallback, _ := s.Attr("fallback")
|
|
f.fallback = fallback
|
|
|
|
method, _ := s.Attr("method")
|
|
f.method = method
|
|
|
|
timeout, ok := s.Attr("timeout")
|
|
if !ok {
|
|
timeout = "60"
|
|
}
|
|
t, _ := strconv.ParseInt(timeout, 10, 64)
|
|
f.timeout = t
|
|
|
|
deferred, ok := s.Attr("deferred")
|
|
f.deferred = ok && strings.ToUpper(deferred) != "FALSE"
|
|
|
|
primary, ok := s.Attr("primary")
|
|
f.primary = ok && strings.ToUpper(primary) != "FALSE"
|
|
|
|
f.head = make([]*html.Node, 0)
|
|
|
|
return f
|
|
}
|
|
|
|
// Src is the URL for the fragment.
|
|
func (f *Fragment) Src() string {
|
|
return f.src
|
|
}
|
|
|
|
// Fallback is the fallback URL for the fragment.
|
|
func (f *Fragment) Fallback() string {
|
|
return f.fallback
|
|
}
|
|
|
|
// Timeout is the timeout for fetching the fragment.
|
|
func (f *Fragment) Timeout() time.Duration {
|
|
return time.Duration(f.timeout) * time.Second
|
|
}
|
|
|
|
// Method is the HTTP method to use for fetching the fragment.
|
|
func (f *Fragment) Method() string {
|
|
return f.method
|
|
}
|
|
|
|
// Element is a pointer to the selected element in the DOM.
|
|
func (f *Fragment) Element() *goquery.Selection {
|
|
return f.s
|
|
}
|
|
|
|
// Deferred is deferring the fetching to the browser.
|
|
func (f *Fragment) Deferred() bool {
|
|
return f.deferred
|
|
}
|
|
|
|
// Primary denotes a fragment as responsible for setting
|
|
// the response code of the entire HTML page.
|
|
func (f *Fragment) Primary() bool {
|
|
return f.primary
|
|
}
|
|
|
|
// Links returns the new nodes that go in the head via
|
|
// the LINK HTTP header entity.
|
|
func (f *Fragment) Links() []*html.Node {
|
|
return f.head
|
|
}
|
|
|
|
// ID returns the identifier of the fragment.
|
|
func (f *Fragment) ID() string {
|
|
return f.id
|
|
}
|
|
|
|
// Ref returns the id of the fragment that this
|
|
// fragment references. This is a forward reference.
|
|
func (f *Fragment) Ref() string {
|
|
return f.ref
|
|
}
|
|
|
|
// Resolve is resolving all needed data, setting headers
|
|
// and the status code.
|
|
func (f *Fragment) Resolve() ResolverFunc {
|
|
return func(c *fiber.Ctx, cfg Config) error {
|
|
err := f.do(c, cfg, f.src)
|
|
if err == nil {
|
|
return err
|
|
}
|
|
|
|
if err != fasthttp.ErrTimeout {
|
|
return err
|
|
}
|
|
|
|
err = f.do(c, cfg, f.fallback)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (f *Fragment) do(c *fiber.Ctx, cfg Config, src string) error {
|
|
req := fasthttp.AcquireRequest()
|
|
res := fasthttp.AcquireResponse()
|
|
|
|
defer fasthttp.ReleaseRequest(req)
|
|
defer fasthttp.ReleaseResponse(res)
|
|
|
|
c.Request().CopyTo(req)
|
|
|
|
uri := fasthttp.AcquireURI()
|
|
defer fasthttp.ReleaseURI(uri)
|
|
|
|
if err := uri.Parse(nil, []byte(src)); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(uri.Host()) == 0 {
|
|
uri.SetHost(cfg.DefaultHost)
|
|
}
|
|
req.SetRequestURI(uri.String())
|
|
req.Header.Del(fiber.HeaderConnection)
|
|
|
|
t := f.Timeout()
|
|
if err := client.DoTimeout(req, res, t); err != nil {
|
|
return err
|
|
}
|
|
|
|
res = cfg.FilterResponse(res)
|
|
f.statusCode = res.StatusCode()
|
|
|
|
if res.StatusCode() != http.StatusOK {
|
|
// TODO: wrap in custom error, to not replace
|
|
return fmt.Errorf("resolve: could not resolve fragment at %s", f.Src())
|
|
}
|
|
|
|
res.Header.Del(fiber.HeaderConnection)
|
|
|
|
contentEncoding := res.Header.Peek("Content-Encoding")
|
|
body := res.Body()
|
|
|
|
var err error
|
|
if bytes.EqualFold(contentEncoding, []byte("gzip")) {
|
|
body, err = res.BodyGunzip()
|
|
if err != nil {
|
|
return cfg.ErrorHandler(c, err)
|
|
}
|
|
}
|
|
|
|
h := Header(string(res.Header.Peek("link")))
|
|
nodes := CreateNodes(h.Links())
|
|
f.head = append(f.head, nodes...)
|
|
|
|
f.s.ReplaceWithHtml(string(body))
|
|
|
|
return nil
|
|
}
|