483 lines
9.7 KiB
Go
483 lines
9.7 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"gitlab.com/golang-commonmark/markdown"
|
|
"html/template"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"p83.nl/go/ekster/pkg/util"
|
|
"p83.nl/go/indieauth"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
mp PagesRepository
|
|
)
|
|
|
|
const (
|
|
ClientID = "https://wiki.p83.nl/"
|
|
RedirectURI = "https://wiki.p83.nl/auth/callback"
|
|
)
|
|
|
|
// Page
|
|
type Page struct {
|
|
Content string
|
|
}
|
|
|
|
type DiffPage struct {
|
|
Content string
|
|
Diff template.HTML
|
|
}
|
|
|
|
type Revision struct {
|
|
Version string
|
|
Page DiffPage
|
|
Summary string
|
|
}
|
|
|
|
type Change struct {
|
|
Page string
|
|
Date time.Time
|
|
EndDate time.Time
|
|
Body string
|
|
Count int
|
|
|
|
DateSummary string
|
|
TimeSummary string
|
|
}
|
|
|
|
type PagesRepository interface {
|
|
Get(p string) Page
|
|
Save(p string, page Page, summary, author string) error
|
|
Exist(p string) bool
|
|
PageHistory(p string) ([]Revision, error)
|
|
RecentChanges() ([]Change, error)
|
|
}
|
|
|
|
type indexPage struct {
|
|
Session *Session
|
|
Title string
|
|
Name string
|
|
Content template.HTML
|
|
}
|
|
|
|
type editPage struct {
|
|
Session *Session
|
|
Title string
|
|
Content string
|
|
Name string
|
|
}
|
|
|
|
type historyPage struct {
|
|
Session *Session
|
|
Title string
|
|
Name string
|
|
History []Revision
|
|
}
|
|
|
|
type recentPage struct {
|
|
Session *Session
|
|
Title string
|
|
Name string
|
|
Recent []Change
|
|
}
|
|
|
|
type indexHandler struct{}
|
|
type saveHandler struct{}
|
|
type editHandler struct{}
|
|
type historyHandler struct{}
|
|
type recentHandler struct{}
|
|
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
|
|
}
|
|
defer func() {
|
|
if err := sess.Flush(); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}()
|
|
|
|
if r.Method == http.MethodGet {
|
|
if r.URL.Path == "/auth/login" {
|
|
t, err := template.ParseFiles("templates/layout.html", "templates/login.html")
|
|
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" {
|
|
log.Printf("%+v\n", sess)
|
|
code := r.URL.Query().Get("code")
|
|
state := r.URL.Query().Get("state")
|
|
|
|
if state != sess.State {
|
|
log.Printf("mismatched state: %s != %s", state, sess.State)
|
|
http.Error(w, "mismatched state", 500)
|
|
return
|
|
}
|
|
authURL := sess.AuthorizationEndpoint
|
|
|
|
verified, response, err := indieauth.VerifyAuthCode(ClientID, code, RedirectURI, authURL)
|
|
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" {
|
|
t, err := template.ParseFiles("templates/layout.html", "templates/logout.html")
|
|
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
|
|
sess.RedirectURI = RedirectURI
|
|
sess.NextURI = "/"
|
|
sess.State = state
|
|
|
|
newURL := indieauth.CreateAuthenticationURL(*authURL, urlString, ClientID, RedirectURI, state)
|
|
|
|
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
|
|
}
|
|
defer func() {
|
|
if err := sess.Flush(); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}()
|
|
|
|
if !sess.LoggedIn {
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
|
return
|
|
}
|
|
|
|
r.ParseForm()
|
|
page := r.URL.Path[9:]
|
|
if page == "" {
|
|
page = "Home"
|
|
}
|
|
|
|
history, err := mp.PageHistory(page)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
return
|
|
}
|
|
|
|
funcs := template.FuncMap{
|
|
"historyIndex": func(x int, history []Revision) int { return len(history) - x },
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
err = t.Execute(w, historyPage{
|
|
Session: sess,
|
|
Title: "History of " + strings.Replace(page, "_", " ", -1),
|
|
Name: page,
|
|
History: history,
|
|
})
|
|
if err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
return
|
|
}
|
|
}
|
|
|
|
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)
|
|
return
|
|
}
|
|
defer func() {
|
|
if err := sess.Flush(); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}()
|
|
|
|
if !sess.LoggedIn {
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
|
return
|
|
}
|
|
|
|
r.ParseForm()
|
|
page := r.PostForm.Get("p")
|
|
summary := r.PostForm.Get("summary")
|
|
|
|
pageData := Page{Content: r.PostForm.Get("content")}
|
|
err = mp.Save(page, pageData, summary, sess.Me)
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
http.Redirect(w, r, "/"+page, http.StatusFound)
|
|
}
|
|
|
|
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
|
|
}
|
|
defer func() {
|
|
if err := sess.Flush(); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}()
|
|
|
|
if !sess.LoggedIn {
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
|
return
|
|
}
|
|
|
|
page := r.URL.Path[6:]
|
|
if page == "" {
|
|
page = "Home"
|
|
}
|
|
|
|
pageText := mp.Get(page).Content
|
|
|
|
data := editPage{
|
|
Session: sess,
|
|
Title: strings.Replace(page, "_", " ", -1),
|
|
Content: pageText,
|
|
Name: page,
|
|
}
|
|
|
|
t, err := template.ParseFiles("templates/layout.html", "templates/edit.html")
|
|
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
|
|
}
|
|
}
|
|
|
|
func (h *indexHandler) 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)
|
|
}
|
|
}()
|
|
|
|
page := r.URL.Path[1:]
|
|
if page == "" {
|
|
page = "Home"
|
|
}
|
|
|
|
pageText := mp.Get(page).Content
|
|
if pageText == "" {
|
|
http.Redirect(w, r, "/edit/"+page, 302)
|
|
return
|
|
}
|
|
|
|
hrefRE := regexp.MustCompile(`\[\[\s*([\w- ]+)\s*\]\]`)
|
|
|
|
pageText = hrefRE.ReplaceAllStringFunc(pageText, func(s string) string {
|
|
s = strings.TrimPrefix(s, "[[")
|
|
s = strings.TrimSuffix(s, "]]")
|
|
s = strings.TrimSpace(s)
|
|
if !mp.Exist(s) {
|
|
// return fmt.Sprintf("<a href=%q class=%q>%s</a>", s, "edit", s)
|
|
return fmt.Sprintf("%s[?](/%s)", s, strings.Replace(s, " ", "_", -1))
|
|
}
|
|
return fmt.Sprintf("[%s](/%s)", s, strings.Replace(s, " ", "_", -1))
|
|
})
|
|
|
|
md := markdown.New(
|
|
markdown.HTML(true),
|
|
markdown.XHTMLOutput(true),
|
|
)
|
|
pageText = md.RenderToString([]byte(pageText))
|
|
|
|
data := indexPage{
|
|
Session: sess,
|
|
Title: strings.Replace(page, "_", " ", -1),
|
|
Content: template.HTML(pageText),
|
|
Name: page,
|
|
}
|
|
|
|
t, err := template.ParseFiles("templates/layout.html", "templates/view.html")
|
|
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
|
|
}
|
|
|
|
}
|
|
|
|
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
|
|
}
|
|
defer func() {
|
|
if err := sess.Flush(); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}()
|
|
|
|
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 {
|
|
f := groupRecentChanges(changes)
|
|
changes = changes[f:]
|
|
}
|
|
|
|
for i, c := range changes {
|
|
if c.Count <= 1 {
|
|
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"))
|
|
}
|
|
}
|
|
|
|
t, err := template.New("layout.html").ParseFiles("templates/layout.html", "templates/recent.html")
|
|
if err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
return
|
|
}
|
|
|
|
err = t.Execute(w, recentPage{
|
|
Session: sess,
|
|
Title: "Recent changes",
|
|
Name: "Recent changes",
|
|
Recent: changes,
|
|
})
|
|
if err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
return
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
var port int
|
|
flag.IntVar(&port, "port", 8080, "http port")
|
|
flag.Parse()
|
|
|
|
mp = NewFilePages("data")
|
|
|
|
http.Handle("/auth/", &authHandler{})
|
|
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("ui/node_modules/trix/dist/"))))
|
|
http.Handle("/save/", &saveHandler{})
|
|
http.Handle("/edit/", &editHandler{})
|
|
http.Handle("/history/", &historyHandler{})
|
|
http.Handle("/recent/", &recentHandler{})
|
|
http.Handle("/", &indexHandler{})
|
|
|
|
fmt.Printf("Running on port %d\n", port)
|
|
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
|
|
}
|