Browse Source

note editing implemented

master
Christian Müller 8 years ago
parent
commit
59aeab60c1
  1. 13
      assets/templates/form.html
  2. 4
      assets/templates/stats.html
  3. 29
      server.go
  4. 60
      storage.go

13
assets/public/new.html → assets/templates/form.html

@ -1,7 +1,8 @@
{{define "Form"}}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>NoteHub &mdash; Add note</title> <title>NoteHub &mdash; {{if .ID}}Edit{{else}}Add{{end}} note</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport"> <meta content="width=device-width, initial-scale=1.0" name="viewport">
<link href="/style.css" rel="stylesheet" type="text/css" /> <link href="/style.css" rel="stylesheet" type="text/css" />
@ -9,13 +10,10 @@
</head> </head>
<body> <body>
<form action="/note" autocomplete="off" method="POST" target="_self"> <form action="/note" autocomplete="off" method="POST" target="_self">
<textarea autofocus name="text"></textarea> <textarea autofocus name="text">{{.Text}}</textarea>
<fieldset> <fieldset>
<input class="ui-elem" <input id="id" name="id" value="{{.ID}}" type="hidden" />
name="plain-password" <input class="ui-elem" id="password" name="password" placeholder="Password for editing" type="text" autocomplete="off">
placeholder="Password for editing"
type="text"
autocomplete="off">
<label style="margin-right: 1em"> <label style="margin-right: 1em">
<input id="tos" name="tos" type="checkbox" onClick="toggleButton()"> <input id="tos" name="tos" type="checkbox" onClick="toggleButton()">
Accept <a href="/TOS.md">Terms of Service</a> Accept <a href="/TOS.md">Terms of Service</a>
@ -33,3 +31,4 @@
</script> </script>
</body> </body>
</html> </html>
{{end}}

4
assets/templates/stats.html

@ -1,8 +1,8 @@
{{define "Stats"}} {{define "Stats"}}
<h2>Statistics</h2> <h2>Statistics</h2>
<table> <table>
<tr><td>Published</td><td>{{.Published}}</td></tr> <tr><td>Published</td><td>{{.Published.Format "Jan 02, 2006 15:04:05 UTC"}}</td></tr>
<tr><td>Edited</td><td>{{.Edited}}</td></tr> <tr><td>Edited</td><td>{{.Edited.Format "Jan 02, 2006 15:04:05 UTC"}}</td></tr>
<tr><td>Views</td><td>{{.Views}}</td></tr> <tr><td>Views</td><td>{{.Views}}</td></tr>
</table> </table>
{{end}} {{end}}

29
server.go

@ -35,21 +35,24 @@ func main() {
e.File("/robots.txt", "assets/public/robots.txt") e.File("/robots.txt", "assets/public/robots.txt")
e.File("/style.css", "assets/public/style.css") e.File("/style.css", "assets/public/style.css")
e.File("/index.html", "assets/public/index.html") e.File("/index.html", "assets/public/index.html")
e.File("/new", "assets/public/new.html")
e.File("/", "assets/public/index.html") e.File("/", "assets/public/index.html")
e.GET("/TOS.md", func(c echo.Context) error { e.GET("/TOS.md", func(c echo.Context) error {
n, code := md2html(c, "TOS") n, code := md2html(c, "TOS")
return c.Render(code, "Page", n) return c.Render(code, "Page", n)
}) })
e.GET("/:id", func(c echo.Context) error { e.GET("/:id", func(c echo.Context) error {
n, code := load(c, db) n, code := load(c, db)
n.prepare()
return c.Render(code, "Note", n) return c.Render(code, "Note", n)
}) })
e.GET("/:id/export", func(c echo.Context) error { e.GET("/:id/export", func(c echo.Context) error {
n, code := load(c, db) n, code := load(c, db)
return c.String(code, n.Text) return c.String(code, n.Text)
}) })
e.GET("/:id/stats", func(c echo.Context) error { e.GET("/:id/stats", func(c echo.Context) error {
n, code := load(c, db) n, code := load(c, db)
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
@ -58,6 +61,15 @@ func main() {
return c.Render(code, "Note", n) return c.Render(code, "Note", n)
}) })
e.GET("/:id/edit", func(c echo.Context) error {
n, code := load(c, db)
return c.Render(code, "Form", n)
})
e.GET("/new", func(c echo.Context) error {
return c.Render(http.StatusOK, "Form", nil)
})
e.POST("/note", func(c echo.Context) error { e.POST("/note", func(c echo.Context) error {
vals, err := c.FormParams() vals, err := c.FormParams()
if err != nil { if err != nil {
@ -73,18 +85,25 @@ func main() {
return c.Render(code, "Note", return c.Render(code, "Note",
errPage(code, "note length not accepted")) errPage(code, "note length not accepted"))
} }
n := Note{ id := get(vals, "id")
n := &Note{
ID: id,
Text: text, Text: text,
Password: get(vals, "password"), Password: get(vals, "password"),
} }
id, err := save(c, db, &n) n, err = save(c, db, n)
if err != nil { if err != nil {
c.Logger().Error(err) c.Logger().Error(err)
code := http.StatusServiceUnavailable code := http.StatusServiceUnavailable
if err == errorUnathorised {
code = http.StatusUnauthorized
} else if err == errorBadRequest {
code = http.StatusBadRequest
}
return c.Render(code, "Note", errPage(code)) return c.Render(code, "Note", errPage(code))
} }
c.Logger().Infof("new note %q created", n.ID) c.Logger().Infof("note %q saved", n.ID)
return c.Redirect(http.StatusMovedPermanently, "/"+id) return c.Redirect(http.StatusMovedPermanently, "/"+n.ID)
}) })
e.Logger.Fatal(e.Start(":3000")) e.Logger.Fatal(e.Start(":3000"))

60
storage.go

@ -2,7 +2,9 @@ package main
import ( import (
"bytes" "bytes"
"crypto/sha256"
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"html/template" "html/template"
"math/rand" "math/rand"
@ -24,12 +26,17 @@ const idLength = 5
var ( var (
errorCodes = map[int]string{ errorCodes = map[int]string{
400: "Bad request", 400: "Bad request",
401: "Unauthorized",
404: "Not found", 404: "Not found",
412: "Precondition failed", 412: "Precondition failed",
503: "Service unavailable", 503: "Service unavailable",
} }
rexpNewLine = regexp.MustCompile("[\n\r]") rexpNewLine = regexp.MustCompile("[\n\r]")
rexpNonAlphaNum = regexp.MustCompile("[`~!@#$%^&*_|+=?;:'\",.<>{}\\/]") rexpNonAlphaNum = regexp.MustCompile("[`~!@#$%^&*_|+=?;:'\",.<>{}\\/]")
rexpNoScriptIframe = regexp.MustCompile("<.*?(iframe|script).*?>")
errorUnathorised = errors.New("id or password is wrong")
errorBadRequest = errors.New("password is empty")
) )
type Note struct { type Note struct {
@ -50,35 +57,69 @@ func errPage(code int, details ...string) Note {
} }
} }
func (n *Note) render() { func (n *Note) prepare() {
fstLine := rexpNewLine.Split(n.Text, -1)[0] fstLine := rexpNewLine.Split(n.Text, -1)[0]
maxLength := 25 maxLength := 25
if len(fstLine) < 25 { if len(fstLine) < 25 {
maxLength = len(fstLine) maxLength = len(fstLine)
} }
n.Text = rexpNoScriptIframe.ReplaceAllString(n.Text, "")
n.Title = strings.TrimSpace(rexpNonAlphaNum.ReplaceAllString(fstLine[:maxLength], "")) n.Title = strings.TrimSpace(rexpNonAlphaNum.ReplaceAllString(fstLine[:maxLength], ""))
n.Password = ""
n.Content = mdTmplHTML([]byte(n.Text)) n.Content = mdTmplHTML([]byte(n.Text))
} }
func save(c echo.Context, db *sql.DB, n *Note) (string, error) { func save(c echo.Context, db *sql.DB, n *Note) (*Note, error) {
if n.Password != "" {
n.Password = fmt.Sprintf("%x", sha256.Sum256([]byte(n.Password)))
}
if n.ID == "" {
return insert(c, db, n)
}
return update(c, db, n)
}
func update(c echo.Context, db *sql.DB, n *Note) (*Note, error) {
if n.Password == "" {
return nil, errorBadRequest
}
tx, err := db.Begin()
if err != nil {
return nil, err
}
stmt, _ := tx.Prepare("update notes set (text, edited) = (?, ?) where id = ? and password = ?")
defer stmt.Close()
res, err := stmt.Exec(n.Text, time.Now(), n.ID, n.Password)
if err != nil {
tx.Rollback()
return nil, err
}
rows, err := res.RowsAffected()
if rows != 1 {
tx.Rollback()
return nil, errorUnathorised
}
return n, tx.Commit()
}
func insert(c echo.Context, db *sql.DB, n *Note) (*Note, error) {
tx, err := db.Begin() tx, err := db.Begin()
if err != nil { if err != nil {
return "", err return nil, err
} }
stmt, _ := tx.Prepare("insert into notes(id, text, password) values(?, ?, ?)") stmt, _ := tx.Prepare("insert into notes(id, text, password) values(?, ?, ?)")
defer stmt.Close() defer stmt.Close()
id := randId() id := randId()
_, err = stmt.Exec(id, n.Text, n.Password) _, err = stmt.Exec(id, n.Text, n.Password)
if err != nil { if err != nil {
tx.Rollback()
if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") { if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") {
tx.Rollback()
c.Logger().Infof("collision on id %q", id) c.Logger().Infof("collision on id %q", id)
return save(c, db, n) return save(c, db, n)
} }
return "", err return nil, err
} }
return id, tx.Commit() n.ID = id
return n, tx.Commit()
} }
func randId() string { func randId() string {
@ -119,7 +160,6 @@ func load(c echo.Context, db *sql.DB) (Note, int) {
Published: published, Published: published,
Edited: edited, Edited: edited,
} }
n.render()
return *n, http.StatusOK return *n, http.StatusOK
} }

Loading…
Cancel
Save