package main
import "embed"
import "fmt"
import "html/template"
import "log"
import "net/http"
import "net/url"
import "reflect"
import "strconv"
import "strings"
import "time"
const dashLinks =
template.HTML(
"<p><a href=\"./ntry\">entries</a> • " +
"<a href=\"./init\">import</a> • " +
"<a href=\"./save\">export</a></p>" )
//go:embed template/*
var templateFS embed.FS
var Templates *template.Template
func init() {
var err error
Templates, err = template.ParseFS(templateFS, "template/*.html")
if err != nil {
panic(err)
}
}
func Req2Date(r *http.Request) (date Date, ok bool) {
year, err := strconv.ParseUint(r.PathValue("year"), 10, 32)
if err != nil {
return date, false
}
month, err := strconv.ParseUint(r.PathValue("month"), 10, 32)
if err != nil {
return date, false
}
day, err := strconv.ParseUint(r.PathValue("day"), 10, 32)
if err != nil {
return date, false
}
date = Time2Date(time.Date(int(year), time.Month(month), int(day), 0, 0, 0, 0, time.UTC))
if date.Year != int(year) || date.Month != int(month) || date.Day != int(day) {
return date, false
}
return date, true
}
func NotImplemented(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Not Implemented!", 400)
}
func TestSecHeaders(r *http.Request) bool {
sfm := r.Header.Get("Sec-Fetch-Mode")
if sfm == "websocket" {
return false
} else if sfm == "cors" || sfm == "no-cors" {
sfs := r.Header.Get("Sec-Fetch-Site")
return sfs != "cross-site"
} else {
return true
}
}
func Login(r *http.Request) (jrnl *Jrnl, user string) {
if !TestSecHeaders(r) {
return nil, ""
} else if r.Host != "" && r.Method == "POST" {
origin := r.Header.Get("Origin")
if origin == "" {
origin = r.Header.Get("Referer")
}
if origin != "" {
uri, err := url.Parse(origin)
if err != nil && uri.Host != r.Host {
return nil, ""
}
}
}
user, pass, ok := r.BasicAuth()
if !ok {
return nil, ""
}
jrnl, ok = Jrnls[user]
if !ok {
return nil, user
}
if !jrnl.Passhash.Check(pass) {
return nil, user
} else {
return jrnl, user
}
}
func MustLogin(w http.ResponseWriter, r *http.Request) *Jrnl {
jrnl, user := Login(r)
if jrnl == nil {
if user != "" {
log.Printf("failed login attempt as %s from %s", user, r.RemoteAddr)
}
w.Header().Add("WWW-Authenticate", "Basic realm=\"jrnl\"")
http.Error(w, "Unauthorized!", 401)
return nil
} else {
return jrnl
}
}
type Err500Page struct {
Links template.HTML
Title string
Type reflect.Type
Error error
}
func Err500(w http.ResponseWriter, r *http.Request, jrnl *Jrnl, err error) {
ty := reflect.TypeOf(err)
log.Printf("error 500 serving %s for %s: %s %s", r.URL.Path, jrnl.Id, ty, err)
w.WriteHeader(500)
ReplyTemplate(
w, r, jrnl, "500.html",
Err500Page {
Links: BuildLinks(r, jrnl),
Title: "Jrnl Server Error",
Type: ty,
Error: err } )
}
func NotFoundGet(w http.ResponseWriter, r *http.Request) {
jrnl := MustLogin(w, r)
if jrnl == nil {
return
}
NotFoundPage(w, r, jrnl)
}
type NotFoundPageData struct {
Links template.HTML
Title string
}
func NotFoundPage(w http.ResponseWriter, r *http.Request, jrnl *Jrnl) {
w.WriteHeader(404)
ReplyTemplate(
w, r, jrnl, "404.html",
NotFoundPageData {
Links: BuildLinks(r, jrnl),
Title: "Jrnl Page Not Found" } )
}
func ReplyTemplate(w http.ResponseWriter, r *http.Request, jrnl *Jrnl, tpl string, ctx interface{}) {
var buf strings.Builder
err := Templates.ExecuteTemplate(&buf, tpl, ctx)
if err != nil {
msg := fmt.Sprintf("error in template %s: %s", tpl, err)
log.Println(msg)
Err500(w, r, jrnl, err)
} else {
w.Write([]byte(buf.String()))
}
}
func Redirect(loc string, code int) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
jrnl := MustLogin(w, r)
if jrnl == nil {
return
}
http.Redirect(w, r, loc, code)
}
}
func BuildLinks(r *http.Request, jrnl *Jrnl) template.HTML {
if jrnl == nil {
return template.HTML("")
}
pfx := strings.Repeat("../", strings.Count(r.URL.Path, "/") - 1)
if pfx == "" {
pfx = "./"
}
links := fmt.Sprintf("<a href=\"%sdash\">dash</a>", pfx)
years, _ := jrnl.ListYears()
for i, year := range years {
if i < 5 {
links += " • "
links += fmt.Sprintf(
"<a href=\"%sjrnl/%04d\">jrnl/%04d</a>",
pfx, year, year )
} else {
break
}
}
return template.HTML("<p>" + links + "</p>")
}
func NtriesGet(w http.ResponseWriter, r *http.Request) {
jrnl := MustLogin(w, r)
if jrnl == nil {
return
}
ents, err := jrnl.ListEntries()
if err != nil {
Err500(w, r, jrnl, err)
return
}
vers := make([]EntryVersion, len(ents))
for i, e := range ents {
n, err := e.GetLatest()
if err != nil {
Err500(w, r, jrnl, err)
return
}
v, err := e.GetVersion(n)
if err != nil {
Err500(w, r, jrnl, err)
return
}
vers[i] = v
}
NtriesPage(
w, r, jrnl,
"Jrnl Entry Index",
"This is a log of every jrnl entry you've made.",
vers )
}
type NtriesPageData struct {
Links template.HTML
Title, Blurb string
Ntries []EntryVersion
Pfx string
}
func NtriesPage(w http.ResponseWriter, r *http.Request, jrnl *Jrnl, Title, Blurb string, ntries []EntryVersion) {
pfx := strings.Repeat("../", strings.Count(r.URL.Path, "/") - 1)
ReplyTemplate(
w, r, jrnl, "ntries.html",
NtriesPageData {
Links: BuildLinks(r, jrnl),
Title: Title, Blurb: Blurb,
Ntries: ntries,
Pfx: pfx } )
}
type DashPage struct {
Links template.HTML
Title string
Jrnl *Jrnl
Today, Yesterday Date
}
func Dash(w http.ResponseWriter, r *http.Request) {
jrnl := MustLogin(w, r)
if jrnl == nil {
return
}
today := time.Now().In((*time.Location)(jrnl.Timezone))
yesterday := today.AddDate(0,0,-1)
ReplyTemplate(
w, r, jrnl, "dash.html",
DashPage {
Links: dashLinks,
Title: "Jrnl Dash",
Jrnl: jrnl,
Today: Time2Date(today),
Yesterday: Time2Date(yesterday), } )
}
func GotoRedirect(w http.ResponseWriter, r *http.Request) {
jrnl := MustLogin(w, r)
if jrnl == nil {
return
}
query := r.URL.Query()
pfx, _ := query["pfx"]
dest, _ := query["dest"]
path := strings.Join(pfx, "") + strings.Join(dest, "")
http.Redirect(w, r, path, 301)
}
func JrnlRedirect(w http.ResponseWriter, r *http.Request) {
jrnl := MustLogin(w, r)
if jrnl == nil {
return
}
years, err := jrnl.ListYears()
if err != nil {
http.Error(w, err.Error(), 500)
} else if len(years) > 0 {
http.Redirect(w, r, fmt.Sprintf("%04d", years[0]), 302)
} else {
http.Redirect(w, r, "../init", 302)
}
}
func JrnlGet(w http.ResponseWriter, r *http.Request) {
jrnl := MustLogin(w, r)
if jrnl == nil {
return
}
year, err := strconv.ParseUint(r.PathValue("year"), 10, 32)
if err != nil {
NotFoundPage(w, r, jrnl)
return
}
JrnlPage(w, r, jrnl, int(year))
}
func JrnlPost(w http.ResponseWriter, r *http.Request) {
jrnl := MustLogin(w, r)
if jrnl == nil {
return
}
vers, err := jrnl.PutText(r.FormValue("text"))
if err != nil {
Err500(w, r, jrnl, err)
return
}
urlYear, err := strconv.ParseUint(r.PathValue("year"), 10, 32)
if err != nil {
NotFoundPage(w, r, jrnl)
return
}
if len(vers) == 0 || (len(vers) == 1 && vers[0].Entry.Date.Year == int(urlYear)) {
JrnlPage(w, r, jrnl, int(urlYear))
} else if len(vers) == 1 {
http.Redirect(w, r, fmt.Sprintf("%04d", vers[0].Entry.Date.Year), 302)
} else {
NtriesPage(
w, r, jrnl,
"Jrnl Update",
"The following entries were changed:",
vers )
}
}
type JrnlPageData struct {
Links template.HTML
Title string
Jrnl *Jrnl
Year int
}
func JrnlPage(w http.ResponseWriter, r *http.Request, jrnl *Jrnl, year int) {
ReplyTemplate(
w, r, jrnl, "jrnl.html",
JrnlPageData {
Links: BuildLinks(r, jrnl),
Title: fmt.Sprintf("Jrnl for Year %04d", year),
Jrnl: jrnl,
Year: year} )
}
type InitPageData struct {
Links template.HTML
Title string
}
func InitGet(w http.ResponseWriter, r *http.Request) {
jrnl := MustLogin(w, r)
if jrnl == nil {
return
}
ReplyTemplate(
w, r, jrnl, "init.html",
InitPageData { BuildLinks(r, jrnl), "Jrnl Import" } )
}
func InitPost(w http.ResponseWriter, r *http.Request) {
jrnl := MustLogin(w, r)
if jrnl == nil {
return
}
vers, err := jrnl.PutText(r.FormValue("text"))
if err != nil {
Err500(w, r, jrnl, err)
return
}
var blurb string
if len(vers) < 1 {
blurb = "No entries were imported."
} else if len(vers) == 1 {
blurb = "The following entry was imported:"
} else {
blurb = "The following entries were imported:"
}
NtriesPage(w, r, jrnl, "Jrnl Import", blurb, vers)
}
type SavePageData struct {
Links template.HTML
Title string
Jrnl *Jrnl
}
func SaveGet(w http.ResponseWriter, r *http.Request) {
jrnl := MustLogin(w, r)
if jrnl == nil {
return
}
query := r.URL.Query()
if _, ok := query["download"]; ok {
backup, err := jrnl.GetBackup()
if err != nil {
Err500(w, r, jrnl, err)
return
}
w.Header().Add("Content-Type", "text/plain; charset=UTF-8")
w.Header().Add("Content-Disposition", "attachment; filename=\"backup.txt\"")
w.Write([]byte(backup))
} else {
ReplyTemplate(
w, r, jrnl, "save.html",
SavePageData {
Links: BuildLinks(r, jrnl),
Title: "Jrnl Export",
Jrnl: jrnl } )
}
}
func NtryGet(w http.ResponseWriter, r *http.Request) {
jrnl := MustLogin(w, r)
if jrnl == nil {
return
}
date, ok := Req2Date(r)
if !ok {
NotFoundPage(w, r, jrnl)
return
}
NtryPage(w, r, jrnl, date)
}
func NtryPost(w http.ResponseWriter, r *http.Request) {
jrnl := MustLogin(w, r)
if jrnl == nil {
return
}
date, ok := Req2Date(r)
if !ok {
NotFoundPage(w, r, jrnl)
return
}
_, _, err := jrnl.PutEntry(date, r.FormValue("text"))
if err != nil {
Err500(w, r, jrnl, err)
return
}
NtryPage(w, r, jrnl, date)
}
type NtryPageData struct {
Links template.HTML
Title string
Entry *Entry
}
func NtryPage(w http.ResponseWriter, r *http.Request, jrnl *Jrnl, date Date) {
ent, err := jrnl.GetEntry(date)
if err == NotFound {
NotFoundPage(w, r, jrnl)
return
} else if err != nil {
Err500(w, r, jrnl, err)
return
}
ReplyTemplate(
w, r, jrnl, "ntry.html",
NtryPageData {
Links: BuildLinks(r, jrnl),
Title: fmt.Sprintf("Jrnl Entry for %s", date),
Entry: &ent } )
}