You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wiki/main.go

425 lines
8.5 KiB

package main
import (
"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
}
type PagesRepository interface {
Get(p string) Page
Save(p string, page Page, summary, author string)
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 sess.Flush()
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" {
code := r.FormValue("code")
state := r.FormValue("state")
if 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 sess.Flush()
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 sess.Flush()
if !sess.LoggedIn {
http.Redirect(w, r, "/", http.StatusFound)
return
}
r.ParseForm()
page := r.PostForm.Get("p")
summary := r.PostForm.Get("summary")
mp.Save(page, Page{Content: r.PostForm.Get("content")}, summary, sess.Me)
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 sess.Flush()
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 sess.Flush()
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 sess.Flush()
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
}
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() {
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 8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}