all repos — nirvash @ 034d325ae36cd0f3978721ef7c0370809e2a918f

modular CMS using the quartzgun library

fix styles and build options, add error handling, start StaticFileManager implementation
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCAAdFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAmKhgWUACgkQO3+8IhRO
Y5iHbQ//VfA9t5wdf23xXv02FU18RfswS9rnk0cKT1VEYSzY0nYloYXlxygdZ/Jv
uLD+7Dc8ismtHCvAtU9S0sDER0QR5USuFs1GyejWc3jq66EVHvz8NMDrR0LmT9sW
iu2xVnM5Mgf9SNrLqZWQs429qtw1z+dLAz4zdedW//XP2JX783PnzLeji+qj8RMr
jI9j7Qjb8Rn2jSzLhp+M4utqvILonmIZoE3BNHXoS91V8jg23R7JM+Xasz4Wt8y5
E0Go6pCSau3Q+8qowLLxSOxevlgIDGNFmMZqbNRcEGRaetAlfPHb979y2dCfvaym
TbYQN5l+jojKzfss5UDkoeYnI8Ic0NhijIyLCZ6BpDgpg8IVxCw0R/1TcgZ86ESw
T5b9J7htp5vg8/jrOMVbIUNnPLbsfHikcSIrQy5OLqZrzZ4HxBT7BkUV9up0qX9E
iPLCtSJazf58VvU3CVm4fI6FVkkXfsxSULRzdTw5uxiDhciZXcuqULXVAqcfAjBq
JHegysYUtPc3ZmsvV+ybi2yDdnMvnVmMajKKwvl7z5Faqyj7QpeMHHAsUQx/FfOn
uKrV/rQYRYryAP8TYIiQ+rj3XUGrD5T960PO2NJA+8p7Uw+nyOX735YD0la/j7lb
vvgXF0t1w1mVyJBu/F1+YSqzK0XFJzgrO+yYsN8/1XXJ5z1WkKc=
=M89I
-----END PGP SIGNATURE-----
commit

034d325ae36cd0f3978721ef7c0370809e2a918f

parent

3683e53c2a4db6561a68dddbfc39bd7b6408f539

M archetype/adapter.goarchetype/adapter.go

@@ -1,8 +1,19 @@

package archetype +import ( + "time" +) + type BuildStatus struct { Success bool Message string +} + +type Page struct { + Title string + Content string + Edited time.Time + Error string } type ConfigOption struct {

@@ -18,7 +29,7 @@ BuildOptions() []string

GetConfig() map[ConfigOption]string SetConfig(map[ConfigOption]string) error ListPages() map[string]string - GetPage(string) (Page, error) + GetPage(string) Page FormatPage(string) string FormattingHelp() string CreatePage(slug, title, content string) error
M archetype/eureka.goarchetype/eureka.go

@@ -72,16 +72,25 @@ }

return pages } -func (self *EurekaAdapter) GetPage(filename string) (Page, error) { +func (self *EurekaAdapter) GetPage(filename string) Page { + if strings.Contains(filename, "../") || strings.Contains(filename, "..\\") { + return Page{ + Error: "You cannot escape!", + } + } fullPath := filepath.Join(self.Root, "inc", filename) f, err := os.ReadFile(fullPath) if err != nil { - return Page{}, err + return Page{ + Error: err.Error(), + } } if !strings.HasSuffix(filename, ".htm") { - return Page{}, errors.New("Page file extension is not '.htm'") + return Page{ + Error: "Page file extension is not '.htm'", + } } title := strings.Replace(

@@ -93,7 +102,7 @@ return Page{

Title: title, Content: content, Edited: fileInfo.ModTime(), - }, nil + } } func (self *EurekaAdapter) FormatPage(raw string) string {

@@ -110,6 +119,10 @@ func (self *EurekaAdapter) CreatePage(slug, title, content string) error {

// eureka creates titles from slugs, so we transform the title into the slug slug = strings.ReplaceAll(title, " ", "_") + ".htm" path := filepath.Join(self.Root, "inc", slug) + + if strings.Contains(slug, "../") || strings.Contains(slug, "..\\") { + return errors.New("You cannot escape!") + } _, err := os.Stat(path) if err == nil || !os.IsNotExist(err) {

@@ -127,6 +140,12 @@

func (self *EurekaAdapter) SavePage(oldSlug, newSlug, title, content string) error { // eureka creates titles from slugs, so we transform the title into the slug newSlug = strings.ReplaceAll(title, " ", "_") + ".htm" + + if strings.Contains(newSlug, "../") || strings.Contains(newSlug, "..\\") || + strings.Contains(oldSlug, "../") || strings.Contains(oldSlug, "..\\") { + return errors.New("You cannot escape!") + } + f, err := os.Create(filepath.Join(self.Root, "inc", newSlug)) if err != nil { return err

@@ -151,6 +170,10 @@ return nil

} func (self *EurekaAdapter) DeletePage(slug string) error { + if strings.Contains(slug, "../") || strings.Contains(slug, "..\\") { + return errors.New("You cannot escape!") + } + siteRoot := self.Config[ConfigOption{ Name: "SITEROOT", Type: "string",

@@ -164,14 +187,14 @@ return os.Remove(filepath.Join(self.Root, "inc", slug))

} func (self *EurekaAdapter) Build(buildOptions map[string][]string) BuildStatus { - - twtxt := buildOptions["twtxt"][0] - cmdArgs := "" + twtxt := strings.Join(buildOptions["twtxt"], " ") + cmdArgs := []string{} if twtxt != "" { - cmdArgs += "-t " + twtxt + cmdArgs = append(cmdArgs, "-t") + cmdArgs = append(cmdArgs, twtxt) } - cmd := exec.Command("./build.sh", cmdArgs) + cmd := exec.Command("./build.sh", cmdArgs...) cmd.Dir = self.Root out, err := cmd.CombinedOutput()
D archetype/page.go

@@ -1,11 +0,0 @@

-package archetype - -import ( - "time" -) - -type Page struct { - Title string - Content string - Edited time.Time -}
A archetype/staticFileManager.go

@@ -0,0 +1,16 @@

+package archetype + +type StaticFileManager struct { + Root string + ShowHtml bool + ShowHidden bool +} + +type FileManager interface { + Init(cfg Config) error + ListTree() []string + ListSubTree(root string) []string + AddFile(path string, file interface{}) error + MkDir(path string) error + Remove(path string) error +}
M nirvash.gonirvash.go

@@ -3,9 +3,9 @@

import ( "net/http" core "nilfm.cc/git/nirvash/archetype" - shell "nilfm.cc/git/nirvash/lfo" + . "nilfm.cc/git/nirvash/lfo" "nilfm.cc/git/quartzgun/indentalUserDB" - "nilfm.cc/git/quartzgun/middleware" + . "nilfm.cc/git/quartzgun/middleware" "nilfm.cc/git/quartzgun/renderer" "nilfm.cc/git/quartzgun/router" "os"

@@ -40,14 +40,14 @@

rtr.Get("/login", renderer.Template( pathConcat(templateRoot, "login.html"))) - rtr.Post("/login", middleware.Authorize("/", udb, "/login?tryagain=1")) + rtr.Post("/login", Authorize("/", udb, "/login?tryagain=1")) - rtr.Get("/logout", middleware.Bunt("/", udb, "/login?tryagain=1")) + rtr.Get("/logout", Bunt("/", udb, "/login?tryagain=1")) rtr.Get( "/", - middleware.Protected( - shell.WithAdapter( + Protected( + WithAdapter( renderer.Template( pathConcat(templateRoot, "cms_list.html"), pathConcat(templateRoot, "header.html"),

@@ -59,9 +59,9 @@ "/login"))

rtr.Get( `/edit/(?P<Slug>\S+)`, - middleware.Fortify( - middleware.Protected( - shell.WithAdapter( + Fortify( + Protected( + WithAdapter( renderer.Template( pathConcat(templateRoot, "cms_edit.html"), pathConcat(templateRoot, "header.html"),

@@ -73,10 +73,10 @@ "/login")))

rtr.Post( `/save/(?P<Slug>\S+)`, - middleware.Defend( - middleware.Protected( - shell.WithAdapter( - shell.EnsurePageData( + Defend( + Protected( + WithAdapter( + EnsurePageData( renderer.Template( pathConcat(templateRoot, "cms_save.html"), pathConcat(templateRoot, "header.html"),

@@ -91,9 +91,9 @@ "/"))

rtr.Get( `/new`, - middleware.Fortify( - middleware.Protected( - shell.WithAdapter( + Fortify( + Protected( + WithAdapter( renderer.Template( pathConcat(templateRoot, "cms_new.html"), pathConcat(templateRoot, "header.html"),

@@ -105,10 +105,10 @@ "/login")))

rtr.Post( `/create`, - middleware.Defend( - middleware.Protected( - shell.WithAdapter( - shell.EnsurePageData( + Defend( + Protected( + WithAdapter( + EnsurePageData( renderer.Template( pathConcat(templateRoot, "cms_create.html"), pathConcat(templateRoot, "header.html"),

@@ -123,9 +123,9 @@ "/"))

rtr.Get( `/build`, - middleware.Fortify( - middleware.Protected( - shell.WithAdapter( + Fortify( + Protected( + WithAdapter( renderer.Template( pathConcat(templateRoot, "build.html"), pathConcat(templateRoot, "header.html"),

@@ -137,10 +137,10 @@ "/login")))

rtr.Post( `/build-run`, - middleware.Defend( - middleware.Protected( - shell.SanitizeFormMap( - shell.WithAdapter( + Defend( + Protected( + SanitizeFormMap( + WithAdapter( renderer.Template( pathConcat(templateRoot, "build_run.html"), pathConcat(templateRoot, "header.html"),

@@ -154,9 +154,9 @@ "/"))

rtr.Post( `/delete/(?P<Slug>\S+)`, - middleware.Defend( - middleware.Protected( - shell.WithAdapter( + Defend( + Protected( + WithAdapter( renderer.Template( pathConcat(templateRoot, "delete.html"), pathConcat(templateRoot, "header.html"),

@@ -170,9 +170,9 @@ "/"))

rtr.Get( `/config`, - middleware.Fortify( - middleware.Protected( - shell.WithAdapter( + Fortify( + Protected( + WithAdapter( renderer.Template( pathConcat(templateRoot, "config.html"), pathConcat(templateRoot, "header.html"),

@@ -184,11 +184,11 @@ "/login")))

rtr.Post( `/config-set`, - middleware.Defend( - middleware.Protected( - shell.SanitizeFormMap( - shell.FormMapToAdapterConfig( - shell.WithAdapter( + Defend( + Protected( + SanitizeFormMap( + FormMapToAdapterConfig( + WithAdapter( renderer.Template( pathConcat(templateRoot, "config_set.html"), pathConcat(templateRoot, "header.html"),
M static/style.cssstatic/style.css

@@ -3,7 +3,6 @@ padding: 0;

margin: 0; font-family: sans-serif; font-size: 16px; - height: 100vh; background: black; color: white; background: url('/static/bg2.png');

@@ -16,6 +15,7 @@ .login-body {

background: url('/static/bg.png'); background-size: cover; background-position: center center; + height: 100vh; } .login {

@@ -48,7 +48,7 @@ position: absolute;

} -.login form input, .login form input:-internal-autofill-selected { +.login form input, #user-input, #password-input { display: block; margin: 1em; margin-left: auto;

@@ -60,7 +60,7 @@ color: lightgray;

padding: 0.2em; } -.login form input[type="text"], login form input[type="password"] { +.login form input[type="text"], .login form input[type="password"] { transition: border 1s; outline: none; margin-bottom: -17px;

@@ -73,7 +73,6 @@ }

.login form input[type="submit"] { - margin-top: -17px; text-transform: uppercase; transition: background 1s, color 1s; }

@@ -84,7 +83,7 @@ color: black;

} .login .error { - positon: relative; + position: relative; text-align: center; margin-left: auto; margin-right: auto;

@@ -109,11 +108,12 @@ }

nav { text-align: center; - background: rgba(0,0,0,0.8); - padding-top: 0.5em; + background: rgba(0,0,0,0.8); + padding-top: 0.5em; padding-bottom: 0.5em; position: sticky; top: 0px; + z-index: 3; } nav ul {

@@ -136,9 +136,23 @@ a:hover {

color: cyan; } -a.new-page-button { +.new-page-button-wrapper { + display: block; + width: 80%; + max-width: 500px; position: sticky; - top: 0; + top: 5em; + margin-left: auto; + margin-right: auto; + z-index: 2; + text-align: right; + height: 0; + overflow-y: visible; +} + +a.new-page-button { + position: relative; + top: 1em; text-decoration: none; background: transparent; border: solid 2px lightgray;

@@ -147,8 +161,6 @@ color: lightgray;

padding: 0.2em; text-transform: uppercase; transition: background 1s, color 1s; - float: right; - z-index: 2; } a.new-page-button:hover {

@@ -164,8 +176,7 @@ background: rgba(0,0,0,0.8);

display: block; border-left: 8px solid cyan; padding-left: 8px; - position: sticky; - top: 3em; + position: relative; } .page-list, form.editor, form.build, form.configurator, span.adapter-error, span.adapter-success, .danger-zone {

@@ -177,7 +188,6 @@ background: rgba(0,0,0,0.8);

padding: 2em; margin-left: auto; margin-right: auto; - max-height: calc(100vh - 20em); overflow-y: auto; }

@@ -210,6 +220,12 @@ width: 100%;

} + + +form.editor input.slug-input { + font-size: 100%; +} + form.editor input.title-input { font-size: 150%; }

@@ -230,7 +246,7 @@ height: 25em;

} form.configurator textarea { - marign: 0; + margin: 0; width: 100%; height: 5em; }

@@ -272,3 +288,7 @@

form input[hidden] { display: none; } + +form input[readonly] { + border: none; +}
M templates/cms_edit.htmltemplates/cms_edit.html

@@ -6,6 +6,12 @@ {{ $noEmpty := .FormValue "no-empty" }}

{{ template "header" . }} +{{ if ($page).Error }} + <h2>Page Error</h2> + + <span class="adapter-error">{{($page).Error}}</span> +{{ else }} + <h2>Edit Page</h2> <form class="editor" method="POST" action="/save/{{$slug}}">

@@ -14,12 +20,12 @@ <span class="edit-error">Empty fields are not allowed - please try again</span><br/>

{{ end }} <input hidden name="csrfToken" value="{{$csrfToken}}"/> <input hidden name="oldSlug" value="{{$slug}}"/> - {{ if $editableSlugs }} + <label for="title">Title</label><br/> + <input class="title-input" id="title" type="text" name="title" value="{{($page).Title}}" required/><br/> + {{ if $editableSlugs }} <label for="slug">Slug</label><br/> <input class="slug-input" id="slug" type="text" name="slug" value="{{$slug}}" required/><br/> {{ end }} - <label for="title">Title</label><br/> - <input class="title-input" id="title" type="text" name="title" value="{{($page).Title}}" required/><br/> <span class="edited-time">last edited {{($page).Edited.Format "2006-01-02 15:04"}}</span><br/> <label for="content">Content</label><br/> <textarea class="content-input" id="content" name="content" required>{{($page).Content}}</textarea><br/>

@@ -38,4 +44,7 @@ </label><br/>

<input type="submit" value="DELETE"/> </form> </details> + +{{ end }} + {{ template "footer" . }}
M templates/cms_list.htmltemplates/cms_list.html

@@ -4,8 +4,11 @@ {{ template "header" .}}

<h2>Pages</h2> +<div class="new-page-button-wrapper"> + <a class="new-page-button" href="/new">New Page</a> +</div> + <div class="page-list"> - <a class="new-page-button" href="/new">New Page</a> <ul> {{ range $slug, $title := $pages }} <li><a href="/edit/{{$slug}}">{{$title}}</a></li>

@@ -13,6 +16,4 @@ {{ end }}

</ul> </div> -<div class="static-files-list"> -</div> -{{ template "footer" .}}+{{ template "footer" .}}
M templates/cms_new.htmltemplates/cms_new.html

@@ -11,12 +11,12 @@ {{ if $noEmpty }}

<span class="edit-error">Empty fields are not allowed - please try again</span><br/> {{ end }} <input hidden name="csrfToken" value="{{$csrfToken}}"/> + <label for="title">Title</label><br/> + <input class="title-input" id="title" type="text" name="title" required/><br/> {{ if $editableSlugs }} <label for="slug">Slug</label><br/> <input class="slug-input" id="slug" type="text" name="slug" required/><br/> {{ end }} - <label for="title">Title</label><br/> - <input class="title-input" id="title" type="text" name="title" required/><br/> <label for="content">Content</label><br/> <textarea class="content-input" id="content" name="content" required></textarea><br/> <input type="submit" value="Save"/>