225 lines
7.6 KiB
Go
225 lines
7.6 KiB
Go
package editor
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"git.coffeylabs.org/coffey-labs/hotdog-cms/internal/preview"
|
|
"git.coffeylabs.org/coffey-labs/hotdog-cms/internal/site"
|
|
)
|
|
|
|
// Redirects, and moving a page. When a page moves, its old address keeps
|
|
// working: a redirect is added in the same commit, and redirects that
|
|
// pointed at the old address are pointed at the new one, so there are no
|
|
// chains. Redirects live in site.yaml, and only that block is rewritten.
|
|
|
|
func (s *Server) apiRedirects(w http.ResponseWriter, r *http.Request, sess *Session, f *ForgeConfig, id string) {
|
|
st, acc, err := s.siteByID(r.Context(), sess, f, id)
|
|
if s.signedOut(w, r, err) {
|
|
return
|
|
}
|
|
if err != nil || st == nil {
|
|
apiError(w, http.StatusNotFound, "No such site, or you can't open it.")
|
|
return
|
|
}
|
|
branch := r.URL.Query().Get("branch")
|
|
if branch == "" {
|
|
branch = st.Branch
|
|
}
|
|
loaded, _, commit, err := s.ws.checkout(sess, f.Kind, st, s.cloneURL(st), branch)
|
|
if loaded == nil {
|
|
apiError(w, http.StatusBadGateway, "Couldn't load the site: "+trimErr(err))
|
|
return
|
|
}
|
|
list := loaded.Config.Redirects
|
|
if list == nil {
|
|
list = []site.Redirect{}
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"redirects": list, "commit": commit, "branch": branch, "canWrite": acc.Write})
|
|
}
|
|
|
|
// checkRedirects says what's wrong with a list of redirects, or nothing.
|
|
func checkRedirects(list []site.Redirect, pages map[string]bool) error {
|
|
seen := map[string]bool{}
|
|
for i, rd := range list {
|
|
n := i + 1
|
|
switch {
|
|
case !strings.HasPrefix(rd.From, "/") || strings.Contains(rd.From, ".."):
|
|
return fmt.Errorf("redirect %d: %q should be an address on the site, starting with /", n, rd.From)
|
|
case seen[rd.From]:
|
|
return fmt.Errorf("redirect %d: %s is redirected twice", n, rd.From)
|
|
case !strings.HasPrefix(rd.To, "/") && !strings.HasPrefix(rd.To, "https://") && !strings.HasPrefix(rd.To, "http://"):
|
|
return fmt.Errorf("redirect %d: %q should start with / or https://", n, rd.To)
|
|
case rd.From == rd.To:
|
|
return fmt.Errorf("redirect %d: %s redirects to itself", n, rd.From)
|
|
case pages[rd.From] || pages[strings.TrimSuffix(rd.From, "/")+"/"]:
|
|
return fmt.Errorf("redirect %d: %s is a page; a redirect there would hide it", n, rd.From)
|
|
}
|
|
seen[rd.From] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func redirectsValue(list []site.Redirect) []map[string]string {
|
|
out := []map[string]string{}
|
|
for _, rd := range list {
|
|
out = append(out, map[string]string{"from": rd.From, "to": rd.To})
|
|
}
|
|
return out
|
|
}
|
|
|
|
type redirectsRequest struct {
|
|
Branch string `json:"branch"`
|
|
BaseCommit string `json:"baseCommit"`
|
|
Redirects []site.Redirect `json:"redirects"`
|
|
}
|
|
|
|
func (s *Server) redirectsSave(w http.ResponseWriter, r *http.Request, sess *Session, f *ForgeConfig, st *SiteConfig) {
|
|
var req redirectsRequest
|
|
if !decode(w, r, &req) {
|
|
return
|
|
}
|
|
if req.Branch == "" {
|
|
req.Branch = st.Branch
|
|
}
|
|
loaded, siteDir, _, err := s.ws.checkout(sess, f.Kind, st, s.cloneURL(st), req.Branch)
|
|
if loaded == nil {
|
|
s.writeError(w, r, err)
|
|
return
|
|
}
|
|
for i := range req.Redirects {
|
|
req.Redirects[i].From = strings.TrimSpace(req.Redirects[i].From)
|
|
req.Redirects[i].To = strings.TrimSpace(req.Redirects[i].To)
|
|
}
|
|
if err := checkRedirects(req.Redirects, pagePaths(loaded)); err != nil {
|
|
apiError(w, http.StatusUnprocessableEntity, err.Error())
|
|
return
|
|
}
|
|
cfg, err := readIn(siteDir, "site.yaml")
|
|
if err != nil {
|
|
apiError(w, http.StatusBadGateway, "Couldn't read site.yaml.")
|
|
return
|
|
}
|
|
out, err := setTopLevel(string(cfg), "redirects", redirectsValue(req.Redirects))
|
|
if err != nil {
|
|
apiError(w, http.StatusUnprocessableEntity, err.Error())
|
|
return
|
|
}
|
|
target, create := req.Branch, false
|
|
if req.Branch == st.Branch {
|
|
target, create = s.draftName(r, sess, f, st, "redirects.md"), true
|
|
}
|
|
commit, err := s.ws.save(sess, f.Kind, st, s.cloneURL(st), req.Branch, target, create, req.BaseCommit, map[string][]byte{repoFile(st, "site.yaml"): []byte(out)}, "Update redirects")
|
|
if err != nil {
|
|
s.writeError(w, r, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"branch": target, "commit": commit, "created": create})
|
|
}
|
|
|
|
func pagePaths(s *site.Site) map[string]bool {
|
|
out := map[string]bool{}
|
|
for _, p := range s.Pages {
|
|
if p.RedirectTo == "" {
|
|
out[p.Path] = true
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
type moveRequest struct {
|
|
Branch string `json:"branch"`
|
|
BaseCommit string `json:"baseCommit"`
|
|
Source string `json:"source"`
|
|
To string `json:"to"` // the new source path, content/…md
|
|
}
|
|
|
|
// movePage renames a page's file, and redirects its old address to its new
|
|
// one, in one commit.
|
|
func (s *Server) movePage(w http.ResponseWriter, r *http.Request, sess *Session, f *ForgeConfig, st *SiteConfig) {
|
|
var req moveRequest
|
|
if !decode(w, r, &req) {
|
|
return
|
|
}
|
|
if req.Branch == "" {
|
|
req.Branch = st.Branch
|
|
}
|
|
loaded, siteDir, _, err := s.ws.checkout(sess, f.Kind, st, s.cloneURL(st), req.Branch)
|
|
if loaded == nil {
|
|
s.writeError(w, r, err)
|
|
return
|
|
}
|
|
content, err := readSource(siteDir, req.Source)
|
|
if err != nil {
|
|
apiError(w, http.StatusNotFound, "No such page on this branch.")
|
|
return
|
|
}
|
|
to := path.Clean(strings.TrimSpace(req.To))
|
|
if _, err := readSource(siteDir, to); err == nil {
|
|
apiError(w, http.StatusConflict, "There's already a page at "+to+".")
|
|
return
|
|
}
|
|
if c := filepath.ToSlash(filepath.Clean("/" + to))[1:]; c != to || !strings.HasPrefix(to, "content/") || !strings.HasSuffix(to, ".md") || to == req.Source {
|
|
apiError(w, http.StatusBadRequest, "The new place should be a different file under content/, ending in .md.")
|
|
return
|
|
}
|
|
oldPath, newPath := "", ""
|
|
for _, p := range loaded.Pages {
|
|
if "content/"+p.Source == req.Source {
|
|
oldPath = p.Path
|
|
}
|
|
}
|
|
// Where the page will be, worked out by the build's own rules.
|
|
moved, err := site.Load(siteDir, site.Options{Drafts: true, Skip: map[string]bool{strings.TrimPrefix(req.Source, "content/"): true}, Overrides: map[string][]byte{strings.TrimPrefix(to, "content/"): content}})
|
|
if err != nil {
|
|
apiError(w, http.StatusUnprocessableEntity, "The page wouldn't load there: "+trimErr(err))
|
|
return
|
|
}
|
|
for _, p := range moved.Pages {
|
|
if "content/"+p.Source == to {
|
|
newPath = p.Path
|
|
}
|
|
}
|
|
files := map[string][]byte{repoFile(st, req.Source): nil, repoFile(st, to): content}
|
|
redirected := false
|
|
if oldPath != "" && newPath != "" && oldPath != newPath {
|
|
list := []site.Redirect{}
|
|
for _, rd := range loaded.Config.Redirects {
|
|
if rd.From == newPath {
|
|
continue // the new address is a page now
|
|
}
|
|
if rd.To == oldPath {
|
|
rd.To = newPath // no chains
|
|
}
|
|
list = append(list, rd)
|
|
}
|
|
list = append(list, site.Redirect{From: oldPath, To: newPath})
|
|
cfg, err := readIn(siteDir, "site.yaml")
|
|
if err != nil {
|
|
apiError(w, http.StatusBadGateway, "Couldn't read site.yaml.")
|
|
return
|
|
}
|
|
out, err := setTopLevel(string(cfg), "redirects", redirectsValue(list))
|
|
if err != nil {
|
|
apiError(w, http.StatusUnprocessableEntity, err.Error())
|
|
return
|
|
}
|
|
files[repoFile(st, "site.yaml")] = []byte(out)
|
|
redirected = true
|
|
}
|
|
target, create := req.Branch, false
|
|
if req.Branch == st.Branch {
|
|
target, create = s.draftName(r, sess, f, st, to), true
|
|
}
|
|
commit, err := s.ws.save(sess, f.Kind, st, s.cloneURL(st), req.Branch, target, create, req.BaseCommit, files, fmt.Sprintf("Move %s to %s", req.Source, to))
|
|
if err != nil {
|
|
s.writeError(w, r, err)
|
|
return
|
|
}
|
|
s.log.Printf("%s moved %s to %s on %s@%s (%s)", sess.Login, req.Source, to, st.ID, target, shortSHA(commit))
|
|
writeJSON(w, http.StatusOK, map[string]any{"branch": target, "commit": commit, "created": create, "source": to, "from": oldPath, "to": newPath, "redirected": redirected, "preview": st.Preview.url(preview.Slug(target))})
|
|
}
|