package main import ( "encoding/json" "flag" "fmt" "html" "html/template" "log" "net/http" "net/url" "regexp" "strings" "time" "p83.nl/go/ekster/pkg/util" "p83.nl/go/indieauth" ) func init() { log.SetFlags(log.Lshortfile) } var ( mp PagesRepository port = flag.Int("port", 8080, "listen port") baseurl = flag.String("baseurl", "", "baseurl") redirectURI string = "" ) type Backref struct { Name string Title string LineHTML template.HTML Line string } // Page type Page struct { // Name is the filename of the page Name string // Title is the human-readable title of the page Title string Content string Refs map[string][]Backref } 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 pageBaseInfo struct { BaseURL string RedirectURI string } type indexPage struct { pageBaseInfo Session *Session Title string Name string Content template.HTML Backrefs map[string][]Backref } type editPage struct { pageBaseInfo Session *Session Title string Content string Name string Editor template.HTML Backrefs map[string][]Backref } type historyPage struct { pageBaseInfo Session *Session Title string Name string History []Revision } type recentPage struct { pageBaseInfo 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" { 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(*baseurl, 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, *baseurl, 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 } pageBase := getPageBase() pageData := historyPage{ pageBaseInfo: pageBase, Session: sess, Title: "History of " + cleanTitle(page), Name: page, History: history, } err = t.Execute(w, pageData) if err != nil { http.Error(w, err.Error(), 500) return } } func getPageBase() pageBaseInfo { clientID := *baseurl pageBase := pageBaseInfo{ BaseURL: clientID, RedirectURI: redirectURI, } return pageBase } 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 } err = r.ParseForm() if err != nil { http.Error(w, err.Error(), 500) return } isJson := r.PostForm.Get("json") == "1" 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) } if isJson { fmt.Print(w, "{}") } else { 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" } mpPage := mp.Get(page) pageText := mpPage.Content if pageText == "" { pageText = "[]" } var rawMsg json.RawMessage err = json.NewDecoder(strings.NewReader(pageText)).Decode(&rawMsg) jsonEditor := pageText != "" && err == nil var editor template.HTML if jsonEditor { editor, err = renderEditor(page, pageText, "json") if err != nil { http.Error(w, err.Error(), 500) return } } else { editor = template.HTML(fmt.Sprintf(`
`, html.EscapeString(pageText))) } pageBase := getPageBase() data := editPage{ pageBaseInfo: pageBase, Session: sess, Title: cleanTitle(page), Content: pageText, Editor: editor, Name: page, Backrefs: mpPage.Refs, } 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" } mpPage := mp.Get(page) pageText := mpPage.Content if pageText == "" { http.Redirect(w, r, "/edit/"+page, 302) return } if pageText == "" { pageText = "[]" } var rawMsg json.RawMessage err = json.NewDecoder(strings.NewReader(pageText)).Decode(&rawMsg) jsonPage := pageText != "" && err == nil if jsonPage { var listItems []struct { Indented int Text string } err = json.NewDecoder(strings.NewReader(pageText)).Decode(&listItems) if err != nil { http.Error(w, err.Error(), 500) return } pageText = "" for _, item := range listItems { pageText += strings.Repeat(" ", item.Indented) + "* " + item.Text + "\n" } } pageText = renderLinks(pageText) pageText = renderMarkdown2(pageText) pageBase := getPageBase() data := indexPage{ pageBaseInfo: pageBase, Session: sess, Title: cleanTitle(page), Content: template.HTML(pageText), Name: page, Backrefs: mpPage.Refs, } 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 { log.Println(err) // http.Error(w, err.Error(), 500) return } } func renderLinks(pageText string) string { 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(`%s`, cleanNameURL(s), s) } return fmt.Sprintf("[%s](/%s)", s, cleanNameURL(s)) }) return pageText } 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 } pageBase := getPageBase() err = t.Execute(w, recentPage{ pageBaseInfo: pageBase, Session: sess, Title: "Recent changes", Name: "Recent changes", Recent: changes, }) if err != nil { http.Error(w, err.Error(), 500) return } } 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"` } func main() { flag.Parse() *baseurl = strings.TrimRight(*baseurl, "/") + "/" redirectURI = fmt.Sprintf("%sauth/callback", *baseurl) mp = NewFilePages("data") http.Handle("/auth/", &authHandler{}) http.HandleFunc("/links.json", func(w http.ResponseWriter, r *http.Request) { type Document struct { Title string `json:"title"` } var results []Document pages, err := mp.(*FilePages).AllPages() if err != nil { http.Error(w, err.Error(), 500) return } for _, page := range pages { results = append(results, Document{page.Title}) } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(&results) if err != nil { http.Error(w, err.Error(), 500) return } }) http.HandleFunc("/documents.json", func(w http.ResponseWriter, r *http.Request) { type Document struct { Title string `json:"title"` Body string `json:"body"` URL string `json:"url"` } var results []Document pages, err := mp.(*FilePages).AllPages() if err != nil { http.Error(w, err.Error(), 500) return } for _, page := range pages { content := strings.Builder{} var listItems []struct { Indented int Text string } err = json.NewDecoder(strings.NewReader(page.Content)).Decode(&listItems) if err == nil { for _, item := range listItems { content.WriteString(item.Text) content.WriteByte(' ') } } else { content.WriteString(page.Content) content.WriteByte(' ') } for page, refs := range page.Refs { content.WriteString(page) content.WriteByte(' ') for _, ref := range refs { content.WriteString(ref.Line) content.WriteByte(' ') } } results = append(results, Document{ Title: page.Title, Body: content.String(), URL: page.Name, }) } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(&results) if err != nil { http.Error(w, err.Error(), 500) return } }) http.HandleFunc("/fetchLink", func(w http.ResponseWriter, r *http.Request) { link := r.URL.Query().Get("url") u, err := url.Parse(link) if err != nil { http.Error(w, err.Error(), 400) return } var response LinkResponse response.Success = 1 response.Link = u.String() response.Meta.Title = "Test" json.NewEncoder(w).Encode(response) }) http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("./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)) }