wiki/main.go

1245 lines
27 KiB
Go
Raw Normal View History

2018-11-24 12:34:51 +00:00
package main
import (
"bytes"
2021-01-17 16:36:17 +00:00
"context"
2019-08-25 10:30:00 +00:00
"encoding/json"
2019-02-18 19:47:55 +00:00
"flag"
2018-11-24 12:34:51 +00:00
"fmt"
2019-08-25 10:56:11 +00:00
"html"
2018-11-24 12:34:51 +00:00
"html/template"
2020-06-11 20:20:17 +00:00
"io"
2018-11-24 12:34:51 +00:00
"log"
"net/http"
"net/url"
"os"
"path/filepath"
2018-11-24 12:34:51 +00:00
"regexp"
"strings"
2019-02-18 19:47:31 +00:00
"time"
2019-08-25 10:30:00 +00:00
"github.com/blevesearch/bleve"
2019-08-25 10:30:00 +00:00
"p83.nl/go/ekster/pkg/util"
"p83.nl/go/indieauth"
2018-11-24 12:34:51 +00:00
)
func init() {
log.SetFlags(log.Lshortfile)
}
2021-01-17 16:36:17 +00:00
type authorizedKey string
var (
authKey = authorizedKey("authorizedKey")
)
2018-11-24 12:34:51 +00:00
var (
mp PagesRepository
2021-01-17 16:36:17 +00:00
port = flag.Int("port", 8080, "listen port")
baseurl = flag.String("baseurl", "", "baseurl")
redirectURI = ""
authToken = os.Getenv("API_TOKEN")
2018-11-24 12:34:51 +00:00
)
2021-02-11 20:20:38 +00:00
type Day struct {
Class string
Text string
URL string
Count string
}
2020-05-08 15:21:39 +00:00
type Backref struct {
Name string
Title string
LineHTML template.HTML
LineEditHTML template.HTML
Line string
2020-05-08 15:21:39 +00:00
}
2018-11-24 12:34:51 +00:00
// Page
type Page struct {
2020-05-07 13:20:36 +00:00
// Name is the filename of the page
Name string
// Title is the human-readable title of the page
Title string
2018-11-24 12:34:51 +00:00
Content string
2020-05-07 13:20:36 +00:00
2020-10-21 18:49:23 +00:00
Refs map[string][]Backref
Blocks BlockResponse
Parent string
2018-11-24 12:34:51 +00:00
}
type DiffPage struct {
Content string
Diff template.HTML
}
type Revision struct {
Version string
Page DiffPage
Summary string
}
2019-02-18 19:47:31 +00:00
type Change struct {
2019-03-02 13:38:35 +00:00
Page string
Date time.Time
EndDate time.Time
Body string
Count int
DateSummary string
TimeSummary string
2019-02-18 19:47:31 +00:00
}
2018-11-24 12:34:51 +00:00
type PagesRepository interface {
Get(p string) Page
2019-02-19 06:34:52 +00:00
Save(p string, page Page, summary, author string) error
2018-11-24 12:34:51 +00:00
Exist(p string) bool
PageHistory(p string) ([]Revision, error)
2019-02-18 19:47:31 +00:00
RecentChanges() ([]Change, error)
AllPages() ([]Page, error)
2018-11-24 12:34:51 +00:00
}
2020-05-18 19:12:49 +00:00
type pageBaseInfo struct {
BaseURL string
RedirectURI string
2021-02-11 20:20:38 +00:00
Days []Day
Month string
2020-05-18 19:12:49 +00:00
}
2018-11-24 12:34:51 +00:00
type indexPage struct {
2020-05-18 19:12:49 +00:00
pageBaseInfo
2020-07-19 13:09:15 +00:00
Session *Session
Title string
Name string
Content template.HTML
Backrefs map[string][]Backref
ShowGraph bool
2020-07-19 13:18:03 +00:00
TodayPage string
2018-11-24 12:34:51 +00:00
}
type Node struct {
2020-07-05 19:18:42 +00:00
Id int `json:"id"`
Label string `json:"label"`
Color *string `json:"color"`
2020-07-07 22:45:06 +00:00
Opacity float64 `json:"opacity"`
}
type Edge struct {
From int `json:"from"`
To int `json:"to"`
}
type graphPage struct {
pageBaseInfo
Session *Session
Title string
Name string
Nodes template.JS
Edges template.JS
}
2020-10-21 18:49:23 +00:00
type Parent struct {
Text string
ID string
}
2018-11-24 12:34:51 +00:00
type editPage struct {
2020-05-18 19:12:49 +00:00
pageBaseInfo
2020-07-19 13:09:15 +00:00
Session *Session
Title string
Content string
Name string
Editor template.HTML
Backrefs map[string][]Backref
ShowGraph bool
2020-07-19 13:18:03 +00:00
TodayPage string
2020-10-21 18:49:23 +00:00
Parent Parent
Parents []Parent
2018-11-24 12:34:51 +00:00
}
type historyPage struct {
2020-05-18 19:12:49 +00:00
pageBaseInfo
2018-11-24 12:34:51 +00:00
Session *Session
Title string
Name string
History []Revision
}
2019-02-18 19:47:31 +00:00
type recentPage struct {
2020-05-18 19:12:49 +00:00
pageBaseInfo
2019-02-18 19:47:31 +00:00
Session *Session
Title string
Name string
2019-02-19 06:34:52 +00:00
Recent []Change
2019-02-18 19:47:31 +00:00
}
2021-02-11 20:20:38 +00:00
var baseTemplate = []string{"templates/layout.html", "templates/sidebar.html", "templates/sidebar-right.html"}
2020-08-05 08:03:54 +00:00
2018-11-24 12:34:51 +00:00
type indexHandler struct{}
type graphHandler struct{}
2020-07-01 14:40:10 +00:00
type saveHandler struct {
SearchIndex bleve.Index
}
2018-11-24 12:34:51 +00:00
type editHandler struct{}
type historyHandler struct{}
2019-02-18 19:47:31 +00:00
type recentHandler struct{}
2018-11-24 12:34:51 +00:00
type authHandler struct{}
func (*authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sess, err := NewSession(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
2019-02-25 18:03:04 +00:00
defer func() {
if err := sess.Flush(); err != nil {
log.Println(err)
}
}()
2018-11-24 12:34:51 +00:00
if r.Method == http.MethodGet {
if r.URL.Path == "/auth/login" {
2020-08-05 08:03:54 +00:00
templates := baseTemplate
templates = append(templates, "templates/login.html")
t, err := template.ParseFiles(templates...)
2018-11-24 12:34:51 +00:00
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = t.Execute(w, nil)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
} else if r.URL.Path == "/auth/callback" {
2019-02-25 19:09:43 +00:00
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
2018-11-24 12:34:51 +00:00
if state != sess.State {
2019-02-25 19:09:43 +00:00
log.Printf("mismatched state: %s != %s", state, sess.State)
2018-11-24 12:34:51 +00:00
http.Error(w, "mismatched state", 500)
return
}
authURL := sess.AuthorizationEndpoint
2020-05-18 19:58:44 +00:00
verified, response, err := indieauth.VerifyAuthCode(*baseurl, code, redirectURI, authURL)
2018-11-24 12:34:51 +00:00
if err != nil {
http.Error(w, err.Error(), 500)
return
}
if !verified {
http.Redirect(w, r, "/auth/login", 302)
return
}
sess.Me = response.Me
sess.LoggedIn = true
sess.State = ""
http.Redirect(w, r, sess.NextURI, 302)
return
} else if r.URL.Path == "/auth/logout" {
2020-08-05 08:03:54 +00:00
templates := baseTemplate
templates = append(templates, "templates/logout.html")
t, err := template.ParseFiles(templates...)
2018-11-24 12:34:51 +00:00
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = t.Execute(w, nil)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
} else if r.Method == http.MethodPost {
if r.URL.Path == "/auth/login" {
r.ParseForm()
urlString := r.PostFormValue("url")
u, err := url.Parse(urlString)
if err != nil {
http.Error(w, err.Error(), 400)
return
}
endpoints, err := indieauth.GetEndpoints(u)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
authURL, err := url.Parse(endpoints.AuthorizationEndpoint)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
state := util.RandStringBytes(32)
sess.AuthorizationEndpoint = authURL.String()
sess.Me = urlString
sess.LoggedIn = false
2020-05-18 19:58:44 +00:00
sess.RedirectURI = redirectURI
2018-11-24 12:34:51 +00:00
sess.NextURI = "/"
sess.State = state
2020-05-18 19:58:44 +00:00
newURL := indieauth.CreateAuthenticationURL(*authURL, urlString, *baseurl, redirectURI, state)
2018-11-24 12:34:51 +00:00
http.Redirect(w, r, newURL, 302)
return
} else if r.URL.Path == "/auth/logout" {
sess.LoggedIn = false
sess.Me = ""
sess.AuthorizationEndpoint = ""
http.Redirect(w, r, "/", 302)
return
}
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (h *historyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sess, err := NewSession(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
2019-02-25 18:03:04 +00:00
defer func() {
if err := sess.Flush(); err != nil {
log.Println(err)
}
}()
2018-11-24 12:34:51 +00:00
2019-02-05 20:22:16 +00:00
if !sess.LoggedIn {
http.Redirect(w, r, "/", http.StatusFound)
return
}
2018-11-24 12:34:51 +00:00
r.ParseForm()
page := r.URL.Path[9:]
2020-08-05 08:11:00 +00:00
page = resolvePageName(page)
2018-11-24 12:34:51 +00:00
history, err := mp.PageHistory(page)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
funcs := template.FuncMap{
2019-02-05 20:22:16 +00:00
"historyIndex": func(x int, history []Revision) int { return len(history) - x },
2018-11-24 12:34:51 +00:00
}
t, err := template.New("layout.html").Funcs(funcs).ParseFiles("templates/layout.html", "templates/history.html")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
2021-02-11 20:20:38 +00:00
pageBase := getPageBase(time.Now())
2020-05-18 19:49:57 +00:00
pageData := historyPage{
pageBaseInfo: pageBase,
Session: sess,
Title: "History of " + cleanTitle(page),
Name: page,
History: history,
}
err = t.Execute(w, pageData)
2018-11-24 12:34:51 +00:00
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
2021-02-11 20:20:38 +00:00
func daysInMonth(year int, month time.Month) int {
return time.Date(year, month, 1, 0, 0, 0, 0, time.UTC).AddDate(0, 1, -1).Day()
}
func prepareDays(t time.Time) []Day {
today := time.Now()
var days []Day
curDate := t.AddDate(0, 0, -t.Day()+1)
preDays := int(curDate.Weekday()) - 1
maxDays := daysInMonth(curDate.Year(), curDate.Month())
endOfMonth := false
for i := 0; i < 6; i++ {
_, week := curDate.ISOWeek()
days = append(days, Day{
Class: "week day",
Text: fmt.Sprintf("%02d", week),
URL: "",
Count: "",
})
for d := 0; d < 7; d++ {
day := -preDays + (i * 7) + 1 + d
fday := fmt.Sprintf("%d", day)
if day <= 0 {
fday = ""
}
if day > maxDays {
fday = ""
endOfMonth = true
}
class := "day"
if timeEqual(curDate, today) {
class += " today"
}
if timeEqual(curDate, t) {
class += " current"
}
days = append(days, Day{
Class: class,
Text: fday,
URL: fmt.Sprintf("/edit/%s", formatDatePageName(curDate)),
Count: "",
})
if fday != "" {
curDate = curDate.AddDate(0, 0, 1)
}
if day == maxDays {
endOfMonth = true
}
}
if endOfMonth {
break
}
}
return days
}
func timeEqual(date time.Time, today time.Time) bool {
if date.Year() != today.Year() {
return false
}
if date.Month() != today.Month() {
return false
}
if date.Day() != today.Day() {
return false
}
return true
}
func getPageBase(t time.Time) pageBaseInfo {
2020-05-18 19:58:44 +00:00
clientID := *baseurl
2020-05-18 19:49:57 +00:00
pageBase := pageBaseInfo{
BaseURL: clientID,
RedirectURI: redirectURI,
2021-02-11 20:20:38 +00:00
Days: prepareDays(t),
Month: t.Month().String(),
2020-05-18 19:49:57 +00:00
}
return pageBase
}
2018-11-24 12:34:51 +00:00
func (h *saveHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sess, err := NewSession(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
2020-07-01 14:40:10 +00:00
log.Println(err)
2018-11-24 12:34:51 +00:00
return
}
2019-02-25 18:03:04 +00:00
defer func() {
if err := sess.Flush(); err != nil {
log.Println(err)
}
}()
2018-11-24 12:34:51 +00:00
2019-02-05 20:22:16 +00:00
if !sess.LoggedIn {
http.Redirect(w, r, "/", http.StatusFound)
return
}
2019-08-25 10:30:00 +00:00
err = r.ParseForm()
if err != nil {
http.Error(w, err.Error(), 500)
2020-07-01 14:40:10 +00:00
log.Println(err)
2019-08-25 10:30:00 +00:00
return
}
isJson := r.PostForm.Get("json") == "1"
2018-11-24 12:34:51 +00:00
page := r.PostForm.Get("p")
summary := r.PostForm.Get("summary")
2019-02-19 06:34:52 +00:00
pageData := Page{Content: r.PostForm.Get("content")}
err = mp.Save(page, pageData, summary, sess.Me)
if err != nil {
log.Println(err)
}
2019-08-25 10:30:00 +00:00
if isJson {
fmt.Print(w, "{}")
} else {
http.Redirect(w, r, "/"+page, http.StatusFound)
}
2018-11-24 12:34:51 +00:00
}
func (h *editHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sess, err := NewSession(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
2019-02-25 18:03:04 +00:00
defer func() {
if err := sess.Flush(); err != nil {
log.Println(err)
}
}()
2018-11-24 12:34:51 +00:00
2019-02-05 20:22:16 +00:00
if !sess.LoggedIn {
http.Redirect(w, r, "/", http.StatusFound)
return
}
2018-11-24 12:34:51 +00:00
page := r.URL.Path[6:]
2020-08-05 08:11:00 +00:00
page = resolvePageName(page)
2018-11-24 12:34:51 +00:00
2020-05-08 15:21:39 +00:00
mpPage := mp.Get(page)
pageText := mpPage.Content
2018-11-24 12:34:51 +00:00
if pageText == "" {
pageText = "[]"
}
var rawMsg json.RawMessage
err = json.NewDecoder(strings.NewReader(pageText)).Decode(&rawMsg)
jsonEditor := pageText != "" && err == nil
2019-08-25 10:30:00 +00:00
var editor template.HTML
if jsonEditor {
2020-06-09 18:36:41 +00:00
editor, err = renderEditor(page, pageText, "json", *baseurl)
2019-08-25 10:30:00 +00:00
if err != nil {
http.Error(w, err.Error(), 500)
return
}
} else {
2019-08-25 10:56:11 +00:00
editor = template.HTML(fmt.Sprintf(`
<textarea name="content" rows="24" style="width:100%%">%s</textarea>
2019-08-25 10:30:00 +00:00
<br>
<div class="field">
<label class="label">Summary</label>
<div class="control">
<input type="text" name="summary" class="input" placeholder="Summary"/>
</div>
</div>
<button class="button is-primary" type="submit">Save</button>
2019-08-25 10:56:11 +00:00
`, html.EscapeString(pageText)))
2019-08-25 10:30:00 +00:00
}
2021-02-11 20:20:38 +00:00
curDate, err := ParseDatePageName(page)
if err != nil {
curDate = time.Now()
}
pageBase := getPageBase(curDate)
2020-10-21 18:49:23 +00:00
title := cleanTitle(mpPage.Title)
if newTitle, err := PageTitle(pageText); err == nil {
title = newTitle
}
2020-10-21 18:49:23 +00:00
var parent Parent
parent.ID = mpPage.Parent
parent.Text = mpPage.Blocks.Texts[parent.ID]
var parents []Parent
for _, p := range mpPage.Blocks.Parents {
var parent Parent
parent.ID = p
parent.Text = mpPage.Blocks.Texts[p]
parents = append(parents, parent)
}
for i, j := 0, len(parents)-1; i < j; i, j = i+1, j-1 {
parents[i], parents[j] = parents[j], parents[i]
}
2018-11-24 12:34:51 +00:00
data := editPage{
2020-05-18 19:49:57 +00:00
pageBaseInfo: pageBase,
2020-05-18 19:58:44 +00:00
Session: sess,
Title: title,
2020-05-18 19:58:44 +00:00
Content: pageText,
Editor: editor,
Name: page,
Backrefs: mpPage.Refs,
2020-08-05 08:11:00 +00:00
TodayPage: "Today",
2020-07-19 13:09:15 +00:00
ShowGraph: page != "Daily_Notes",
2020-10-21 18:49:23 +00:00
Parent: parent,
Parents: parents,
2018-11-24 12:34:51 +00:00
}
2020-08-05 08:03:54 +00:00
templates := baseTemplate
2020-08-31 09:50:03 +00:00
templates = append(templates, "templates/edit.html")
2020-08-05 08:03:54 +00:00
t, err := template.ParseFiles(templates...)
2018-11-24 12:34:51 +00:00
if err != nil {
http.Error(w, err.Error(), 500)
return
}
2020-10-21 18:49:23 +00:00
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=600")
2018-11-24 12:34:51 +00:00
err = t.Execute(w, data)
if err != nil {
2020-07-19 13:18:03 +00:00
log.Println(err)
2018-11-24 12:34:51 +00:00
http.Error(w, err.Error(), 500)
return
}
}
func (h *graphHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sess, err := NewSession(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer func() {
if err := sess.Flush(); err != nil {
log.Println(err)
}
}()
if !sess.LoggedIn {
http.Redirect(w, r, "/", http.StatusFound)
return
}
2020-07-05 12:12:15 +00:00
gb, err := NewGraphBuilder(mp)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
gb.prepareNodeMap()
2020-07-05 12:12:15 +00:00
gb.RemoveNode("DONE")
gb.RemoveNode("TODO")
gb.RemoveNode("Home")
gb.RemoveNode("Projects")
gb.RemoveNode("Notes")
gb.RemoveNode("Books")
gb.RemoveNode("Daily_Notes")
gb.RemoveNode("NewJsonTest")
gb.RemoveNodeWithSuffix("_2020")
2020-07-05 12:12:15 +00:00
err = gb.prepareGraph()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
var nodesBuf bytes.Buffer
var edgesBuf bytes.Buffer
2020-07-05 12:12:15 +00:00
err = json.NewEncoder(&nodesBuf).Encode(&gb.nodes)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
2020-07-05 12:12:15 +00:00
err = json.NewEncoder(&edgesBuf).Encode(&gb.edges)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
2021-02-11 20:20:38 +00:00
pageBase := getPageBase(time.Now())
data := graphPage{
pageBaseInfo: pageBase,
Session: sess,
Title: "Graph",
Name: "Graph",
Nodes: template.JS(nodesBuf.String()),
Edges: template.JS(edgesBuf.String()),
}
2020-08-05 08:03:54 +00:00
templates := baseTemplate
2020-08-31 09:50:03 +00:00
templates = append(templates, "templates/graph.html")
2020-08-05 08:03:54 +00:00
t, err := template.ParseFiles(templates...)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = t.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
2018-11-24 12:34:51 +00:00
func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
metaKV, err := regexp.Compile(`(\w+)::\s+(.*)`)
if err != nil {
log.Fatal(err)
}
2018-11-24 12:34:51 +00:00
sess, err := NewSession(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
2019-02-25 18:03:04 +00:00
defer func() {
if err := sess.Flush(); err != nil {
log.Println(err)
}
}()
2018-11-24 12:34:51 +00:00
if !sess.LoggedIn {
2020-07-04 13:07:32 +00:00
http.Redirect(w, r, "/auth/login", http.StatusFound)
return
}
2020-06-11 20:20:17 +00:00
format := r.URL.Query().Get("format")
if format == "" {
format = "html"
}
if !(format == "html" || format == "markdown" || format == "hmarkdown" || format == "json" || format == "metakv") {
2020-06-11 20:20:17 +00:00
http.Error(w, "unknown format", http.StatusBadRequest)
}
2018-11-24 12:34:51 +00:00
page := r.URL.Path[1:]
2021-01-17 16:36:17 +00:00
if page == "favicon.ico" {
http.Error(w, "Not Found", 404)
return
}
2020-08-05 08:11:00 +00:00
page = resolvePageName(page)
2018-11-24 12:34:51 +00:00
2020-05-07 13:20:36 +00:00
mpPage := mp.Get(page)
pageText := mpPage.Content
2020-10-21 18:49:23 +00:00
if (format == "" || format == "html") && pageText == "" {
2018-11-24 12:34:51 +00:00
http.Redirect(w, r, "/edit/"+page, 302)
return
}
if pageText == "" {
pageText = "[]"
}
2018-11-24 12:34:51 +00:00
var rawMsg json.RawMessage
err = json.NewDecoder(strings.NewReader(pageText)).Decode(&rawMsg)
2021-01-17 16:36:17 +00:00
title := cleanTitle(mpPage.Title)
jsonPage := pageText != "" && err == nil
if jsonPage {
2020-08-31 09:50:03 +00:00
if format == "json" {
2020-10-21 18:49:23 +00:00
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
2020-08-31 09:50:03 +00:00
// Shortcut for json output
_, err := io.WriteString(w, pageText)
if err != nil {
http.Error(w, err.Error(), 500)
}
return
} else if format == "metakv" {
2020-10-21 18:49:23 +00:00
so, err := createStructuredFormat(mpPage)
2020-08-31 09:50:03 +00:00
if err != nil {
http.Error(w, err.Error(), 500)
return
}
2020-10-21 18:49:23 +00:00
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
2020-08-31 09:50:03 +00:00
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
err = enc.Encode(so)
if err != nil {
http.Error(w, err.Error(), 500)
}
return
}
2020-05-11 21:34:10 +00:00
var listItems []struct {
Indented int
Text string
}
err = json.NewDecoder(strings.NewReader(pageText)).Decode(&listItems)
2019-08-25 10:30:00 +00:00
if err != nil {
http.Error(w, err.Error(), 500)
return
2018-11-24 12:34:51 +00:00
}
2020-05-11 21:34:10 +00:00
builder := strings.Builder{}
2021-01-17 13:54:53 +00:00
if format == "markdown" || format == "html" {
for _, item := range listItems {
lines := strings.Split(item.Text, "\n")
if len(lines) > 1 {
first := true
for _, line := range lines {
if first {
builder.WriteString(strings.Repeat(" ", item.Indented))
builder.WriteString("* ")
builder.WriteString(line)
builder.WriteByte('\n')
first = false
} else {
builder.WriteString(strings.Repeat(" ", item.Indented+1))
builder.WriteString(line)
builder.WriteByte('\n')
}
}
} else {
if matches := metaKV.FindStringSubmatch(item.Text); matches != nil {
if matches[1] == "Title" {
title = matches[2]
}
}
builder.WriteString(strings.Repeat(" ", item.Indented))
builder.WriteString("* ")
builder.WriteString(item.Text)
builder.WriteByte('\n')
}
}
} else if format == "hmarkdown" {
for _, item := range listItems {
builder.WriteString(item.Text)
builder.WriteByte('\n')
builder.WriteByte('\n')
}
2020-05-11 21:34:10 +00:00
}
pageText = builder.String()
2019-08-25 10:30:00 +00:00
}
2020-06-11 20:20:17 +00:00
if format == "html" {
2020-07-01 15:36:36 +00:00
pageText = metaKV.ReplaceAllString(pageText, "**[[$1]]**: $2")
pageText = renderLinks(pageText, false)
2020-06-11 20:20:17 +00:00
pageText = renderMarkdown2(pageText)
2020-05-11 21:34:10 +00:00
2021-02-11 20:20:38 +00:00
curDate, err := ParseDatePageName(page)
if err != nil {
curDate = time.Now()
}
pageBase := getPageBase(curDate)
2020-06-11 20:20:17 +00:00
data := indexPage{
pageBaseInfo: pageBase,
Session: sess,
Title: title,
2020-06-11 20:20:17 +00:00
Content: template.HTML(pageText),
Name: page,
Backrefs: mpPage.Refs,
2020-07-19 13:09:15 +00:00
ShowGraph: page != "Daily_Notes",
2020-08-05 08:11:00 +00:00
TodayPage: "Today",
2020-06-11 20:20:17 +00:00
}
2020-08-31 09:50:03 +00:00
templates := baseTemplate
templates = append(templates, "templates/view.html")
2020-08-05 08:03:54 +00:00
t, err := template.ParseFiles(templates...)
2020-06-11 20:20:17 +00:00
if err != nil {
http.Error(w, err.Error(), 500)
return
}
2020-10-21 18:49:23 +00:00
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=600")
2020-06-11 20:20:17 +00:00
err = t.Execute(w, data)
if err != nil {
log.Println(err)
// http.Error(w, err.Error(), 500)
return
}
} else if format == "markdown" || format == "hmarkdown" {
2020-10-21 18:49:23 +00:00
w.Header().Set("Content-Type", "text/markdown")
w.Header().Set("Cache-Control", "public, max-age=600")
2020-06-11 20:20:17 +00:00
_, err = io.WriteString(w, "# ")
_, err = io.WriteString(w, title)
2020-06-11 20:20:17 +00:00
_, err = io.WriteString(w, "\n\n")
_, err = io.WriteString(w, pageText)
2018-11-24 12:34:51 +00:00
}
}
func renderLinks(pageText string, edit bool) string {
2020-05-18 20:09:48 +00:00
hrefRE, err := regexp.Compile(`#?\[\[\s*([^\]]+)\s*\]\]`)
if err != nil {
log.Fatal(err)
}
pageText = hrefRE.ReplaceAllStringFunc(pageText, func(s string) string {
tag := false
if s[0] == '#' {
s = strings.TrimPrefix(s, "#[[")
tag = true
} else {
s = strings.TrimPrefix(s, "[[")
}
s = strings.TrimSuffix(s, "]]")
s = strings.TrimSpace(s)
if tag {
return fmt.Sprintf(`<a href=%q class="tag">%s</a>`, cleanNameURL(s), s)
}
editPart := ""
if edit {
editPart = "edit/"
}
return fmt.Sprintf("[%s](/%s%s)", s, editPart, cleanNameURL(s))
2020-05-18 20:09:48 +00:00
})
return pageText
}
2019-02-18 19:47:31 +00:00
func (h *recentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sess, err := NewSession(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
2019-02-25 18:03:04 +00:00
defer func() {
if err := sess.Flush(); err != nil {
log.Println(err)
}
}()
2019-02-18 19:47:31 +00:00
if !sess.LoggedIn {
http.Redirect(w, r, "/", http.StatusFound)
return
}
r.ParseForm()
changes, err := mp.RecentChanges()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// group recentchanges on Page
if len(changes) > 0 {
2019-03-03 07:30:04 +00:00
f := groupRecentChanges(changes)
changes = changes[f:]
}
2019-03-02 13:38:35 +00:00
for i, c := range changes {
if c.Count <= 1 {
2019-03-02 13:38:35 +00:00
changes[i].DateSummary = c.Date.Format("Mon, 2 Jan 2006")
changes[i].TimeSummary = c.Date.Format("15:04")
} else {
changes[i].DateSummary = c.Date.Format("Mon, 2 Jan 2006")
changes[i].TimeSummary = fmt.Sprintf("%s - %s", c.Date.Format("15:04"), c.EndDate.Format("15:04"))
}
}
2019-02-18 19:47:31 +00:00
t, err := template.New("layout.html").ParseFiles("templates/layout.html", "templates/recent.html")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
2021-02-11 20:20:38 +00:00
pageBase := getPageBase(time.Now())
2019-02-18 19:47:31 +00:00
err = t.Execute(w, recentPage{
2020-05-18 19:49:57 +00:00
pageBaseInfo: pageBase,
2020-05-18 19:58:44 +00:00
Session: sess,
Title: "Recent changes",
Name: "Recent changes",
Recent: changes,
2019-02-18 19:47:31 +00:00
})
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
2019-08-25 10:30:00 +00:00
type LinkResponseMetaImage struct {
Url string `json:"url"`
}
type LinkResponseMeta struct {
Title string `json:"title"`
Description string `json:"description"`
Image LinkResponseMetaImage `json:"image"`
}
type LinkResponse struct {
Success int `json:"success"`
Link string `json:"link"`
Meta LinkResponseMeta `json:"meta"`
}
2018-11-24 12:34:51 +00:00
func main() {
2019-02-18 19:47:55 +00:00
flag.Parse()
2020-05-18 19:58:44 +00:00
*baseurl = strings.TrimRight(*baseurl, "/") + "/"
redirectURI = fmt.Sprintf("%sauth/callback", *baseurl)
2020-07-01 14:40:10 +00:00
dataDir := "data"
searchIndex, err := createSearchIndex(dataDir, "_page-index")
if err != nil {
log.Fatal(err)
}
defer searchIndex.Close()
sh, err := NewSearchHandler(searchIndex)
if err != nil {
log.Fatal(err)
}
2020-07-01 14:40:10 +00:00
mp = NewFilePages(dataDir, searchIndex)
2018-11-24 12:34:51 +00:00
http.Handle("/auth/", &authHandler{})
2021-01-17 16:36:17 +00:00
http.HandleFunc("/api/block/", wrapAuth(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if !r.Context().Value(authKey).(bool) {
http.Error(w, "Unauthorized", 401)
return
}
if r.Method != "GET" {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
format := r.URL.Query().Get("format")
if format == "" {
format = "json"
}
if !(format == "json" || format == "metakv") {
http.Error(w, "unknown format", http.StatusBadRequest)
}
page := r.URL.Path
page = strings.TrimPrefix(page, "/api/block/")
page = resolvePageName(page)
mpPage := mp.Get(page)
pageText := mpPage.Content
if pageText == "" {
http.NotFound(w, r)
return
}
if format == "json" {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
// Shortcut for json output
_, err := io.WriteString(w, pageText)
if err != nil {
http.Error(w, err.Error(), 500)
}
} else if format == "metakv" {
so, err := createStructuredFormat(mpPage)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
err = enc.Encode(so)
if err != nil {
http.Error(w, err.Error(), 500)
}
}
}))
http.HandleFunc("/api/block/append", wrapAuth(func(w http.ResponseWriter, r *http.Request) {
2020-11-03 20:23:33 +00:00
err := r.ParseForm()
if err != nil {
http.Error(w, err.Error(), 400)
return
}
id := r.Form.Get("id")
if id == "" {
http.Error(w, "missing id", 400)
return
}
page := mp.Get(id)
log.Println(page.Content)
var listItems []ListItem
2021-01-17 16:36:17 +00:00
id = page.Name // Use the name that was actually loaded
2020-11-03 20:23:33 +00:00
err = json.NewDecoder(strings.NewReader(page.Content)).Decode(&listItems)
if err != nil && err != io.EOF {
http.Error(w, fmt.Sprintf("while decoding: %s", err.Error()), 500)
return
}
newId := &ID{"1", true}
2021-01-17 16:36:17 +00:00
generatedID := newId.NewID()
2020-11-03 20:23:33 +00:00
listItems = append(listItems, ListItem{
2021-01-17 16:36:17 +00:00
ID: generatedID,
2020-11-03 20:23:33 +00:00
Indented: 0,
Text: r.Form.Get("text"),
Fleeting: false,
})
var buf bytes.Buffer
err = json.NewEncoder(&buf).Encode(&listItems)
if err != nil {
http.Error(w, fmt.Sprintf("while encoding: %s", err.Error()), 500)
return
}
page.Content = buf.String()
page.Name = id
page.Title = id
err = mp.Save(id, page, "", "")
if err != nil {
http.Error(w, fmt.Sprintf("while saving: %s", err.Error()), 500)
return
}
2021-01-17 16:36:17 +00:00
fmt.Println(generatedID)
2020-11-03 20:23:33 +00:00
return
2021-01-17 16:36:17 +00:00
}))
2020-05-31 20:08:36 +00:00
http.HandleFunc("/links.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
2020-07-01 14:40:10 +00:00
http.ServeFile(w, r, filepath.Join(dataDir, LinksFile))
2020-05-31 20:08:36 +00:00
})
http.HandleFunc("/api/graph", func(w http.ResponseWriter, r *http.Request) {
gb, err := NewGraphBuilder(mp)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
for _, name := range r.URL.Query()["name"] {
err = gb.buildFromCenter(name)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
// Keep a copy of the nodes, buildFromCenter appends to the nodeMap
var nodes []string
for k, _ := range gb.nodeMap {
nodes = append(nodes, k)
}
2020-07-07 22:45:06 +00:00
for _, node := range nodes {
err = gb.buildFromCenter(node)
}
err = gb.prepareGraph()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
type data struct {
Nodes []Node `json:"nodes"`
Edges []Edge `json:"edges"`
}
err = json.NewEncoder(w).Encode(&data{gb.nodes, gb.edges})
if err != nil {
http.Error(w, err.Error(), 500)
return
}
})
http.Handle("/search/", sh)
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("./dist"))))
2018-11-24 12:34:51 +00:00
http.Handle("/save/", &saveHandler{})
http.Handle("/edit/", &editHandler{})
http.Handle("/history/", &historyHandler{})
2019-02-18 19:47:31 +00:00
http.Handle("/recent/", &recentHandler{})
http.Handle("/graph/", &graphHandler{})
2018-11-24 12:34:51 +00:00
http.Handle("/", &indexHandler{})
2020-05-18 19:58:44 +00:00
fmt.Printf("Running on port %d\n", *port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
2018-11-24 12:34:51 +00:00
}
2020-07-01 14:40:10 +00:00
2021-01-17 16:36:17 +00:00
func wrapAuth(handler http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
2021-02-03 00:21:15 +00:00
if auth == "" || (authToken != "" && (auth == "Token "+authToken || auth == "Bearer "+authToken)) {
2021-01-17 16:36:17 +00:00
r = r.WithContext(context.WithValue(r.Context(), authKey, auth != ""))
// auth == "", require cookie in handler
handler.ServeHTTP(w, r)
return
}
http.Error(w, "Authorization Required", 401)
}
}
2020-07-01 14:40:10 +00:00
func createSearchIndex(dataDir, indexName string) (bleve.Index, error) {
indexDir := filepath.Join(dataDir, indexName)
if _, err := os.Stat(indexDir); os.IsNotExist(err) {
indexMapping := bleve.NewIndexMapping()
2020-10-21 18:49:23 +00:00
documentMapping := bleve.NewDocumentMapping()
nameFieldMapping := bleve.NewTextFieldMapping()
nameFieldMapping.Store = true
documentMapping.AddFieldMappingsAt("name", nameFieldMapping)
titleFieldMapping := bleve.NewTextFieldMapping()
titleFieldMapping.Store = true
documentMapping.AddFieldMappingsAt("title", titleFieldMapping)
2020-10-30 23:29:41 +00:00
linkFieldMapping := bleve.NewTextFieldMapping()
linkFieldMapping.Store = true
documentMapping.AddFieldMappingsAt("link", linkFieldMapping)
2020-10-21 18:49:23 +00:00
indexMapping.AddDocumentMapping("block", documentMapping)
2020-07-01 14:40:10 +00:00
searchIndex, err := bleve.New(indexDir, indexMapping)
if err != nil {
return nil, err
}
fp := NewFilePages(dataDir, nil)
pages, err := fp.AllPages()
if err != nil {
return nil, err
}
for _, page := range pages {
2020-10-21 18:49:23 +00:00
searchObjects, err := createSearchObjects(page.Name)
2020-07-01 14:40:10 +00:00
if err != nil {
log.Println(err)
continue
}
2020-10-21 18:49:23 +00:00
for _, so := range searchObjects {
err = searchIndex.Index(so.ID, so)
if err != nil {
return nil, err
}
2020-07-01 14:40:10 +00:00
}
}
return searchIndex, nil
} else {
searchIndex, err := bleve.Open(indexDir)
if err != nil {
return nil, err
}
return searchIndex, nil
}
}
2020-08-05 08:11:00 +00:00
func resolvePageName(name string) string {
if name == "" {
return "Home"
}
if name == "Today" {
return todayPage()
}
return name
}