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("%s", 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 := len(changes) - 1 i := f for { if changes[f].Page == changes[i].Page && changes[f].Date.Truncate(24*time.Hour) == changes[i].Date.Truncate(24*time.Hour) { changes[f].Count++ i-- } else { changes[f].EndDate = changes[i+1].Date f-- changes[f] = changes[i] } if i <= 0 { break } } 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)) }