diff --git a/config/config.go b/config/config.go index b462753..b6cd906 100644 --- a/config/config.go +++ b/config/config.go @@ -63,6 +63,7 @@ type AmConfig struct { Title string `yaml:"title"` Text string `yaml:"text"` } `yaml:"userAgreement"` + ExternalPath string `yaml:"externalPath"` } `yaml:"site"` Database struct { Driver string `yaml:"driver"` @@ -212,7 +213,7 @@ func overlayOptionFlag(loaded, defaulted bool) bool { /* overlayConfig takes two configuration structures and overlays them to create the third. * Parameters: - * dest - Points to the destination copnfiguration structure. + * dest - Points to the destination configuration structure. * loaded - Points to the loaded configuration structure. * defaults - Points to the default configuration structure. */ @@ -225,6 +226,7 @@ func overlayConfig(dest *AmConfig, loaded *AmConfig, defaults *AmConfig) { dest.Site.SessionExpire = overlayString(loaded.Site.SessionExpire, defaults.Site.SessionExpire) dest.Site.UserAgreement.Title = overlayString(loaded.Site.UserAgreement.Title, defaults.Site.UserAgreement.Title) dest.Site.UserAgreement.Text = overlayString(loaded.Site.UserAgreement.Text, defaults.Site.UserAgreement.Text) + dest.Site.ExternalPath = overlayString(loaded.Site.ExternalPath, defaults.Site.ExternalPath) dest.Database.Driver = overlayString(loaded.Database.Driver, defaults.Database.Driver) dest.Database.Dsn = overlayString(loaded.Database.Dsn, defaults.Database.Dsn) dest.Defaults.Language = overlayString(loaded.Defaults.Language, defaults.Defaults.Language) diff --git a/config/default.yaml b/config/default.yaml index 93fdac4..9a07869 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -17,6 +17,7 @@ site: title: "Amsterdam User Agreement" text: > Text of this agreement is TBD. + externalPath: "" database: driver: "mysql" dsn: "amsdb:x00yes2k@tcp(localhost)/amsterdam?parseTime=true&loc=UTC" diff --git a/go.mod b/go.mod index 33307c7..042465b 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/labstack/gommon v0.4.2 github.com/sirupsen/logrus v1.9.3 github.com/tkuchiki/go-timezone v0.2.3 - golang.org/x/text v0.30.0 + golang.org/x/text v0.34.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -29,9 +29,9 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - golang.org/x/crypto v0.38.0 // indirect + golang.org/x/crypto v0.48.0 // indirect golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect - golang.org/x/net v0.40.0 // indirect - golang.org/x/sys v0.33.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sys v0.41.0 // indirect golang.org/x/time v0.11.0 // indirect ) diff --git a/go.sum b/go.sum index 06b9318..6528a70 100644 --- a/go.sum +++ b/go.sum @@ -57,17 +57,25 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/main.go b/main.go index af8db2f..54aedc3 100644 --- a/main.go +++ b/main.go @@ -52,6 +52,15 @@ func setupEcho() *echo.Echo { uiset := []echo.MiddlewareFunc{ui.SessionStoreInjector, ui.ContextCreator, ui.IPBanTest, ui.CookieLoginTest} e.RouteNotFound("/*", ui.AmWrap(AmNotFoundHandler), uiset...) + if config.GlobalConfig.Site.ExternalPath != "" { + root, err := os.OpenRoot(config.GlobalConfig.Site.ExternalPath) + if err != nil { + panic(err) + } + fs := root.FS() + e.StaticFS("/ext", fs) + e.GET("/fext/*", ui.AmWrap(ui.AmStaticFramePage(fs, "/fext/")), uiset...) + } e.Match(GetAndPost, "/TODO/*", ui.AmWrap(NotImplPage), uiset...) e.GET("/img/*", ui.AmServeImage) if config.GlobalConfig.Rendering.VeniceCompatibleImageURLs { diff --git a/ui/static.go b/ui/static.go index 333e505..699ada5 100644 --- a/ui/static.go +++ b/ui/static.go @@ -11,11 +11,15 @@ package ui import ( + "bytes" "embed" + "io" "io/fs" "net/http" + "strings" "github.com/labstack/echo/v4" + "golang.org/x/net/html" ) //go:embed static/* @@ -29,3 +33,92 @@ func AmStaticFileHandler() echo.HandlerFunc { } return echo.WrapHandler(http.StripPrefix("/static/", http.FileServer(http.FS(fsys)))) } + +// extractPlainText extracts all plain text from a HTML tree node. +func extractPlainText(n *html.Node) string { + var sb strings.Builder + var walk func(*html.Node) + walk = func(n *html.Node) { + if n.Type == html.TextNode { + sb.WriteString(n.Data) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + walk(c) + } + } + walk(n) + return sb.String() +} + +// extractInnerHTML extracts the inner HTML from a HTML tree node. +func extractInnerHTML(n *html.Node) string { + var buf bytes.Buffer + for c := n.FirstChild; c != nil; c = c.NextSibling { + html.Render(&buf, c) + } + return buf.String() +} + +// breakUpHTML extracts the title and body from an HTML page. +func breakUpHTML(r io.Reader) (string, string, error) { + doc, err := html.Parse(r) + if err != nil { + return "", "", err + } + title := "" + body := "" + var traverse func(*html.Node) + traverse = func(n *html.Node) { + if n.Type == html.ElementNode { + switch n.Data { + case "title": + body = extractPlainText(n) + case "body": + body = extractInnerHTML(n) + } + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + traverse(c) + } + } + traverse(doc) + return title, body, nil +} + +/* AmStaticFramePage generates a handler that will serve up data from an external filesystem "framed" inside + * the frame with the template engine. + * Parameters: + * staticFS - The filesystem to serve from. + * prefix - The prefix to be stripped off pathnames to feed to the filesystem. + * Returns: + * An AmPageFunc suitable for wrapping and adding to the Echo handlers. + */ +func AmStaticFramePage(staticFS fs.FS, prefix string) AmPageFunc { + return func(ctxt AmContext) (string, any) { + fname := ctxt.URLPath() + if strings.HasPrefix(fname, prefix) { + fname = fname[len(prefix):] + } else { + return "error", "invalid path name" + } + mtype := mimeTypeFromFilename(fname) + ctxt.VarMap().Set("mimeType", mtype) + switch mtype { + case "text/html": + f, err := staticFS.Open(fname) + if err != nil { + return "error", err + } + defer f.Close() + title, body, err := breakUpHTML(f) + if err != nil { + return "error", err + } + ctxt.SetFrameTitle(title) + ctxt.VarMap().Set("title", title) + ctxt.VarMap().Set("data", body) + return "framed", "extern.jet" + } + return "error", "Unknown MIME Type: " + mtype + } +} diff --git a/ui/views/extern.jet b/ui/views/extern.jet new file mode 100644 index 0000000..522b6d9 --- /dev/null +++ b/ui/views/extern.jet @@ -0,0 +1,20 @@ +{* + * Amsterdam Web Communities System + * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + *} +