diff --git a/flag.go b/flag.go
index 68e0c42..77a7d05 100644
--- a/flag.go
+++ b/flag.go
@@ -42,9 +42,9 @@ func parseCliArgs() {
util.URL = "http://0.0.0.0:" + util.ServerPort
}
- util.HomePage = CanonicalName(util.HomePage)
- util.UserHypha = CanonicalName(util.UserHypha)
- util.HeaderLinksHypha = CanonicalName(util.HeaderLinksHypha)
+ util.HomePage = util.CanonicalName(util.HomePage)
+ util.UserHypha = util.CanonicalName(util.UserHypha)
+ util.HeaderLinksHypha = util.CanonicalName(util.HeaderLinksHypha)
switch util.AuthMethod {
case "none":
diff --git a/gemini.go b/gemini.go
index 3ed4ceb..b86c3a4 100644
--- a/gemini.go
+++ b/gemini.go
@@ -11,6 +11,7 @@ import (
"git.sr.ht/~adnano/go-gemini"
"git.sr.ht/~adnano/go-gemini/certificate"
+ "github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/util"
)
@@ -28,13 +29,13 @@ Visit home hypha:
func geminiHypha(w *gemini.ResponseWriter, rq *gemini.Request) {
log.Println(rq.URL)
var (
- hyphaName = geminiHyphaNameFromRq(rq, "page", "hypha")
- data, hyphaExists = HyphaStorage[hyphaName]
- hasAmnt = hyphaExists && data.BinaryPath != ""
- contents string
+ hyphaName = geminiHyphaNameFromRq(rq, "page", "hypha")
+ h = hyphae.ByName(hyphaName)
+ hasAmnt = h.Exists && h.BinaryPath != ""
+ contents string
)
- if hyphaExists {
- fileContentsT, errT := ioutil.ReadFile(data.TextPath)
+ if h.Exists {
+ fileContentsT, errT := ioutil.ReadFile(h.TextPath)
if errT == nil {
md := markup.Doc(hyphaName, string(fileContentsT))
contents = md.AsGemtext()
diff --git a/history/operations.go b/history/operations.go
index a8177ba..b815c28 100644
--- a/history/operations.go
+++ b/history/operations.go
@@ -134,3 +134,11 @@ func (hop *HistoryOp) WithUser(u *user.User) *HistoryOp {
}
return hop
}
+
+func (hop *HistoryOp) HasErrors() bool {
+ return len(hop.Errs) > 0
+}
+
+func (hop *HistoryOp) FirstErrorText() string {
+ return hop.Errs[0].Error()
+}
diff --git a/http_auth.go b/http_auth.go
index cfe0799..a9ba5de 100644
--- a/http_auth.go
+++ b/http_auth.go
@@ -6,6 +6,7 @@ import (
"github.com/bouncepaw/mycorrhiza/templates"
"github.com/bouncepaw/mycorrhiza/user"
+ "github.com/bouncepaw/mycorrhiza/util"
)
func init() {
@@ -39,7 +40,7 @@ func handlerLogoutConfirm(w http.ResponseWriter, rq *http.Request) {
func handlerLoginData(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
- username = CanonicalName(rq.PostFormValue("username"))
+ username = util.CanonicalName(rq.PostFormValue("username"))
password = rq.PostFormValue("password")
err = user.LoginDataHTTP(w, rq, username, password)
)
diff --git a/http_mutators.go b/http_mutators.go
index ea37b9c..e4d6bdb 100644
--- a/http_mutators.go
+++ b/http_mutators.go
@@ -5,6 +5,8 @@ import (
"log"
"net/http"
+ "github.com/bouncepaw/mycorrhiza/history"
+ "github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/templates"
"github.com/bouncepaw/mycorrhiza/user"
@@ -25,182 +27,146 @@ func init() {
http.HandleFunc("/unattach-confirm/", handlerUnattachConfirm)
}
-func handlerUnattachAsk(w http.ResponseWriter, rq *http.Request) {
- log.Println(rq.URL)
- var (
- hyphaName = HyphaNameFromRq(rq, "unattach-ask")
- hd, isOld = HyphaStorage[hyphaName]
- hasAmnt = hd != nil && hd.BinaryPath != ""
- )
- if !hasAmnt {
- HttpErr(w, http.StatusBadRequest, hyphaName, "Cannot unattach", "No attachment attached yet, therefore you cannot unattach")
- log.Println("Rejected (no amnt):", rq.URL)
- return
- } else if ok := user.CanProceed(rq, "unattach-confirm"); !ok {
- HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to unattach attachments")
- log.Println("Rejected (no rights):", rq.URL)
- return
- }
- util.HTTP200Page(w, base("Unattach "+hyphaName+"?", templates.UnattachAskHTML(rq, hyphaName, isOld), user.FromRequest(rq)))
-}
-
-func handlerUnattachConfirm(w http.ResponseWriter, rq *http.Request) {
- log.Println(rq.URL)
- var (
- hyphaName = HyphaNameFromRq(rq, "unattach-confirm")
- hyphaData, isOld = HyphaStorage[hyphaName]
- hasAmnt = hyphaData != nil && hyphaData.BinaryPath != ""
- u = user.FromRequest(rq)
- )
- if !u.CanProceed("unattach-confirm") {
- HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to unattach attachments")
- log.Println("Rejected (no rights):", rq.URL)
- return
- }
- if !hasAmnt {
- HttpErr(w, http.StatusBadRequest, hyphaName, "Cannot unattach", "No attachment attached yet, therefore you cannot unattach")
- log.Println("Rejected (no amnt):", rq.URL)
- return
- } else if !isOld {
- // The precondition is to have the hypha in the first place.
- HttpErr(w, http.StatusPreconditionFailed, hyphaName,
- "Error: no such hypha",
- "Could not unattach this hypha because it does not exist")
- return
- }
- if hop := hyphaData.UnattachHypha(hyphaName, u); len(hop.Errs) != 0 {
- HttpErr(w, http.StatusInternalServerError, hyphaName,
- "Error: could not unattach hypha",
- fmt.Sprintf("Could not unattach this hypha due to internal errors. Server errors: %v
", hop.Errs))
- return
- }
- http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
-}
-
-func handlerRenameAsk(w http.ResponseWriter, rq *http.Request) {
- log.Println(rq.URL)
- var (
- hyphaName = HyphaNameFromRq(rq, "rename-ask")
- _, isOld = HyphaStorage[hyphaName]
- u = user.FromRequest(rq)
- )
- if !u.CanProceed("rename-confirm") {
- HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to rename pages.")
- log.Println("Rejected", rq.URL)
- return
- }
- util.HTTP200Page(w, base("Rename "+hyphaName+"?", templates.RenameAskHTML(rq, hyphaName, isOld), u))
-}
-
-func handlerRenameConfirm(w http.ResponseWriter, rq *http.Request) {
- log.Println(rq.URL)
- var (
- hyphaName = HyphaNameFromRq(rq, "rename-confirm")
- _, isOld = HyphaStorage[hyphaName]
- newName = CanonicalName(rq.PostFormValue("new-name"))
- _, newNameIsUsed = HyphaStorage[newName]
- recursive = rq.PostFormValue("recursive") == "true"
- u = user.FromRequest(rq)
- )
- switch {
- case !u.CanProceed("rename-confirm"):
- HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to rename pages.")
- log.Println("Rejected", rq.URL)
- case newNameIsUsed:
- HttpErr(w, http.StatusBadRequest, hyphaName, "Error: hypha exists",
- fmt.Sprintf("Hypha named %s already exists.", hyphaName, hyphaName))
- case newName == "":
- HttpErr(w, http.StatusBadRequest, hyphaName, "Error: no name",
- "No new name is given.")
- case !isOld:
- HttpErr(w, http.StatusBadRequest, hyphaName, "Error: no such hypha",
- "Cannot rename a hypha that does not exist yet.")
- case !HyphaPattern.MatchString(newName):
- HttpErr(w, http.StatusBadRequest, hyphaName, "Error: invalid name",
- "Invalid new name. Names cannot contain characters ^?!:#@><*|\"\\'&%
")
- default:
- if hop := RenameHypha(hyphaName, newName, recursive, u); len(hop.Errs) != 0 {
- HttpErr(w, http.StatusInternalServerError, hyphaName,
- "Error: could not rename hypha",
- fmt.Sprintf("Could not rename this hypha due to an internal error. Server errors: %v
", hop.Errs))
- } else {
- http.Redirect(w, rq, "/page/"+newName, http.StatusSeeOther)
+func factoryHandlerAsker(
+ actionPath string,
+ asker func(*hyphae.Hypha, *user.User) (error, string),
+ succTitleTemplate string,
+ succPageTemplate func(*http.Request, string, bool) string,
+) func(http.ResponseWriter, *http.Request) {
+ return func(w http.ResponseWriter, rq *http.Request) {
+ log.Println(rq.URL)
+ var (
+ hyphaName = HyphaNameFromRq(rq, actionPath)
+ h = hyphae.ByName(hyphaName)
+ u = user.FromRequest(rq)
+ )
+ if err, errtitle := asker(h, u); err != nil {
+ HttpErr(
+ w,
+ http.StatusInternalServerError,
+ hyphaName,
+ errtitle,
+ err.Error())
+ return
}
+ util.HTTP200Page(
+ w,
+ base(
+ fmt.Sprintf(succTitleTemplate, hyphaName),
+ succPageTemplate(rq, hyphaName, h.Exists),
+ u))
}
}
-// handlerDeleteAsk shows a delete dialog.
-func handlerDeleteAsk(w http.ResponseWriter, rq *http.Request) {
- log.Println(rq.URL)
- var (
- hyphaName = HyphaNameFromRq(rq, "delete-ask")
- _, isOld = HyphaStorage[hyphaName]
- u = user.FromRequest(rq)
- )
- if !u.CanProceed("delete-ask") {
- HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a moderator to delete pages.")
- log.Println("Rejected", rq.URL)
- return
+var handlerUnattachAsk = factoryHandlerAsker(
+ "unattach-ask",
+ func(h *hyphae.Hypha, u *user.User) (error, string) {
+ return h.CanUnattach(u)
+ },
+ "Unattach %s?",
+ templates.UnattachAskHTML,
+)
+
+var handlerDeleteAsk = factoryHandlerAsker(
+ "delete-ask",
+ func(h *hyphae.Hypha, u *user.User) (error, string) {
+ return h.CanDelete(u)
+ },
+ "Delete %s?",
+ templates.DeleteAskHTML,
+)
+
+var handlerRenameAsk = factoryHandlerAsker(
+ "rename-ask",
+ func(h *hyphae.Hypha, u *user.User) (error, string) {
+ return h.CanRename(u)
+ },
+ "Rename %s?",
+ templates.RenameAskHTML,
+)
+
+func factoryHandlerConfirmer(
+ actionPath string,
+ confirmer func(*hyphae.Hypha, *user.User, *http.Request) (*history.HistoryOp, string),
+) func(http.ResponseWriter, *http.Request) {
+ return func(w http.ResponseWriter, rq *http.Request) {
+ log.Println(rq.URL)
+ var (
+ hyphaName = HyphaNameFromRq(rq, "unattach-confirm")
+ h = hyphae.ByName(hyphaName)
+ u = user.FromRequest(rq)
+ )
+ if hop, errtitle := confirmer(h, u, rq); hop.HasErrors() {
+ HttpErr(w, http.StatusInternalServerError, hyphaName,
+ errtitle,
+ hop.FirstErrorText())
+ return
+ }
+ http.Redirect(w, rq, "/hypha/"+hyphaName, http.StatusSeeOther)
}
- util.HTTP200Page(w, base("Delete "+hyphaName+"?", templates.DeleteAskHTML(rq, hyphaName, isOld), u))
}
-// handlerDeleteConfirm deletes a hypha for sure
-func handlerDeleteConfirm(w http.ResponseWriter, rq *http.Request) {
- log.Println(rq.URL)
- var (
- hyphaName = HyphaNameFromRq(rq, "delete-confirm")
- hyphaData, isOld = HyphaStorage[hyphaName]
- u = user.FromRequest(rq)
- )
- if !u.CanProceed("delete-confirm") {
- HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a moderator to delete pages.")
- log.Println("Rejected", rq.URL)
- return
- }
- if !isOld {
- // The precondition is to have the hypha in the first place.
- HttpErr(w, http.StatusPreconditionFailed, hyphaName,
- "Error: no such hypha",
- "Could not delete this hypha because it does not exist.")
- return
- }
- if hop := hyphaData.DeleteHypha(hyphaName, u); len(hop.Errs) != 0 {
- HttpErr(w, http.StatusInternalServerError, hyphaName,
- "Error: could not delete hypha",
- fmt.Sprintf("Could not delete this hypha due to internal errors. Server errors: %v
", hop.Errs))
- return
- }
- http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
-}
+var handlerUnattachConfirm = factoryHandlerConfirmer(
+ "unattach-confirm",
+ func(h *hyphae.Hypha, u *user.User, _ *http.Request) (*history.HistoryOp, string) {
+ return h.UnattachHypha(u)
+ },
+)
+
+var handlerDeleteConfirm = factoryHandlerConfirmer(
+ "delete-confirm",
+ func(h *hyphae.Hypha, u *user.User, _ *http.Request) (*history.HistoryOp, string) {
+ return h.DeleteHypha(u)
+ },
+)
+
+var handlerRenameConfirm = factoryHandlerConfirmer(
+ "rename-confirm",
+ func(oldHypha *hyphae.Hypha, u *user.User, rq *http.Request) (*history.HistoryOp, string) {
+ var (
+ newName = util.CanonicalName(rq.PostFormValue("new-name"))
+ recursive = rq.PostFormValue("recursive") == "true"
+ newHypha = hyphae.ByName(newName)
+ )
+ return oldHypha.RenameHypha(newHypha, recursive, u)
+ },
+)
// handlerEdit shows the edit form. It doesn't edit anything actually.
func handlerEdit(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
- hyphaName = HyphaNameFromRq(rq, "edit")
- hyphaData, isOld = HyphaStorage[hyphaName]
- warning string
- textAreaFill string
- err error
- u = user.FromRequest(rq)
+ hyphaName = HyphaNameFromRq(rq, "edit")
+ h = hyphae.ByName(hyphaName)
+ warning string
+ textAreaFill string
+ err error
+ u = user.FromRequest(rq)
)
- if !u.CanProceed("edit") {
- HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.")
- log.Println("Rejected", rq.URL)
+ if err, errtitle := h.CanEdit(u); err != nil {
+ HttpErr(w, http.StatusInternalServerError, hyphaName,
+ errtitle,
+ err.Error())
return
}
- if isOld {
- textAreaFill, err = FetchTextPart(hyphaData)
+ if h.Exists {
+ textAreaFill, err = h.FetchTextPart()
if err != nil {
log.Println(err)
- HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", "Could not fetch text data")
+ HttpErr(w, http.StatusInternalServerError, hyphaName,
+ "Error",
+ "Could not fetch text data")
return
}
} else {
- warning = `
You are creating a new hypha.
` + warning = `You are creating a new hypha.
` } - util.HTTP200Page(w, base("Edit "+hyphaName, templates.EditHTML(rq, hyphaName, textAreaFill, warning), u)) + util.HTTP200Page( + w, + base( + "Edit "+hyphaName, + templates.EditHTML(rq, hyphaName, textAreaFill, warning), + u)) } // handlerUploadText uploads a new text part for the hypha. @@ -208,62 +174,75 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) var ( hyphaName = HyphaNameFromRq(rq, "upload-text") + h = hyphae.ByName(hyphaName) textData = rq.PostFormValue("text") action = rq.PostFormValue("action") u = user.FromRequest(rq) + hop *history.HistoryOp + errtitle string ) - if !u.CanProceed("upload-text") { - HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.") - log.Println("Rejected", rq.URL) - return - } - if textData == "" { - HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No text data passed") + + if action != "Preview" { + hop, errtitle = h.UploadText([]byte(textData), u) + } + + if hop.HasErrors() { + HttpErr(w, http.StatusForbidden, hyphaName, + errtitle, + hop.FirstErrorText()) return } + if action == "Preview" { - util.HTTP200Page(w, base("Preview "+hyphaName, templates.PreviewHTML(rq, hyphaName, textData, "", markup.Doc(hyphaName, textData).AsHTML()), u)) - } else if hop := UploadText(hyphaName, textData, u); len(hop.Errs) != 0 { - HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error()) + util.HTTP200Page( + w, + base( + "Preview "+hyphaName, + templates.PreviewHTML( + rq, + hyphaName, + textData, + "", + markup.Doc(hyphaName, textData).AsHTML()), + u)) } else { - http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther) + http.Redirect(w, rq, "/hypha/"+hyphaName, http.StatusSeeOther) } } // handlerUploadBinary uploads a new binary part for the hypha. func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) - var ( - hyphaName = HyphaNameFromRq(rq, "upload-binary") - u = user.FromRequest(rq) - ) - if !u.CanProceed("upload-binary") { - HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to upload attachments.") - log.Println("Rejected", rq.URL) - return - } - rq.ParseMultipartForm(10 << 20) // Set upload limit - file, handler, err := rq.FormFile("binary") - if file != nil { - defer file.Close() + var ( + hyphaName = HyphaNameFromRq(rq, "upload-binary") + h = hyphae.ByName(hyphaName) + u = user.FromRequest(rq) + file, handler, err = rq.FormFile("binary") + ) + if err, errtitle := h.CanAttach(err, u); err != nil { + HttpErr(w, http.StatusInternalServerError, hyphaName, + errtitle, + err.Error()) } // If file is not passed: if err != nil { - HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No binary data passed") return } // If file is passed: + if file != nil { + defer file.Close() + } var ( - mime = handler.Header.Get("Content-Type") - hop = UploadBinary(hyphaName, mime, file, u) + mime = handler.Header.Get("Content-Type") + hop, errtitle = h.UploadBinary(mime, file, u) ) - if len(hop.Errs) != 0 { - HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error()) + if hop.HasErrors() { + HttpErr(w, http.StatusInternalServerError, hyphaName, errtitle, hop.FirstErrorText()) return } - http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther) + http.Redirect(w, rq, "/hypha/"+hyphaName, http.StatusSeeOther) } diff --git a/http_readers.go b/http_readers.go index 08a5057..0146c52 100644 --- a/http_readers.go +++ b/http_readers.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/bouncepaw/mycorrhiza/history" + "github.com/bouncepaw/mycorrhiza/hyphae" "github.com/bouncepaw/mycorrhiza/markup" "github.com/bouncepaw/mycorrhiza/mimetype" "github.com/bouncepaw/mycorrhiza/templates" @@ -33,7 +34,7 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) { shorterUrl = strings.TrimPrefix(rq.URL.Path, "/rev/") firstSlashIndex = strings.IndexRune(shorterUrl, '/') revHash = shorterUrl[:firstSlashIndex] - hyphaName = CanonicalName(shorterUrl[firstSlashIndex+1:]) + hyphaName = util.CanonicalName(shorterUrl[firstSlashIndex+1:]) contents = fmt.Sprintf(`This hypha had no text at this revision.
`) TextPath = hyphaName + ".myco" textContents, err = history.FileAtRevision(TextPath, revHash) @@ -42,7 +43,7 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) { if err == nil { contents = markup.Doc(hyphaName, textContents).AsHTML() } - treeHTML, _, _ := tree.Tree(hyphaName, IterateHyphaNamesWith) + treeHTML, _, _ := tree.Tree(hyphaName) page := templates.RevisionHTML( rq, hyphaName, @@ -60,10 +61,10 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) { func handlerText(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) hyphaName := HyphaNameFromRq(rq, "text") - if data, ok := HyphaStorage[hyphaName]; ok { - log.Println("Serving", data.TextPath) + if h := hyphae.ByName(hyphaName); h.Exists { + log.Println("Serving", h.TextPath) w.Header().Set("Content-Type", "text/plain; charset=utf-8") - http.ServeFile(w, rq, data.TextPath) + http.ServeFile(w, rq, h.TextPath) } } @@ -71,10 +72,10 @@ func handlerText(w http.ResponseWriter, rq *http.Request) { func handlerBinary(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) hyphaName := HyphaNameFromRq(rq, "binary") - if data, ok := HyphaStorage[hyphaName]; ok { - log.Println("Serving", data.BinaryPath) - w.Header().Set("Content-Type", mimetype.FromExtension(filepath.Ext(data.BinaryPath))) - http.ServeFile(w, rq, data.BinaryPath) + if h := hyphae.ByName(hyphaName); h.Exists { + log.Println("Serving", h.BinaryPath) + w.Header().Set("Content-Type", mimetype.FromExtension(filepath.Ext(h.BinaryPath))) + http.ServeFile(w, rq, h.BinaryPath) } } @@ -82,26 +83,26 @@ func handlerBinary(w http.ResponseWriter, rq *http.Request) { func handlerHypha(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) var ( - hyphaName = HyphaNameFromRq(rq, "page", "hypha") - data, hyphaExists = HyphaStorage[hyphaName] - hasAmnt = hyphaExists && data.BinaryPath != "" - contents string - openGraph string - u = user.FromRequest(rq) + hyphaName = HyphaNameFromRq(rq, "page", "hypha") + h = hyphae.ByName(hyphaName) + hasAmnt = h.Exists && h.BinaryPath != "" + contents string + openGraph string + u = user.FromRequest(rq) ) - if hyphaExists { - fileContentsT, errT := ioutil.ReadFile(data.TextPath) - _, errB := os.Stat(data.BinaryPath) + if h.Exists { + fileContentsT, errT := ioutil.ReadFile(h.TextPath) + _, errB := os.Stat(h.BinaryPath) if errT == nil { md := markup.Doc(hyphaName, string(fileContentsT)) contents = md.AsHTML() openGraph = md.OpenGraphHTML() } if !os.IsNotExist(errB) { - contents = binaryHtmlBlock(hyphaName, data) + contents + contents = h.BinaryHtmlBlock() + contents } } - treeHTML, prevHypha, nextHypha := tree.Tree(hyphaName, IterateHyphaNamesWith) + treeHTML, prevHypha, nextHypha := tree.Tree(hyphaName) util.HTTP200Page(w, templates.BaseHTML( util.BeautifulName(hyphaName), diff --git a/hypha.go b/hypha.go deleted file mode 100644 index b1437b6..0000000 --- a/hypha.go +++ /dev/null @@ -1,326 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "io/ioutil" - "log" - "mime/multipart" - "os" - "path/filepath" - "regexp" - - "github.com/bouncepaw/mycorrhiza/history" - "github.com/bouncepaw/mycorrhiza/hyphae" - "github.com/bouncepaw/mycorrhiza/markup" - "github.com/bouncepaw/mycorrhiza/mimetype" - "github.com/bouncepaw/mycorrhiza/user" - "github.com/bouncepaw/mycorrhiza/util" -) - -func init() { - markup.HyphaExists = func(hyphaName string) bool { - _, hyphaExists := HyphaStorage[hyphaName] - return hyphaExists - } - markup.HyphaAccess = func(hyphaName string) (rawText, binaryBlock string, err error) { - if hyphaData, ok := HyphaStorage[hyphaName]; ok { - rawText, err = FetchTextPart(hyphaData) - if hyphaData.BinaryPath != "" { - binaryBlock = binaryHtmlBlock(hyphaName, hyphaData) - } - } else { - err = errors.New("Hypha " + hyphaName + " does not exist") - } - return - } - markup.HyphaIterate = IterateHyphaNamesWith - markup.HyphaImageForOG = func(hyphaName string) string { - if hd, isOld := GetHyphaData(hyphaName); isOld && hd.BinaryPath != "" { - return util.URL + "/binary/" + hyphaName - } - return util.URL + "/favicon.ico" - } -} - -// GetHyphaData finds a hypha addressed by `hyphaName` and returns its `hyphaData`. `hyphaData` is set to a zero value if this hypha does not exist. `isOld` is false if this hypha does not exist. -func GetHyphaData(hyphaName string) (hyphaData *HyphaData, isOld bool) { - hyphaData, isOld = HyphaStorage[hyphaName] - if hyphaData == nil { - hyphaData = &HyphaData{} - } - return -} - -// HyphaData represents a hypha's meta information: binary and text parts rooted paths and content types. -type HyphaData hyphae.Hypha - -// uploadHelp is a helper function for UploadText and UploadBinary -func uploadHelp(hop *history.HistoryOp, hyphaName, ext string, data []byte, u *user.User) *history.HistoryOp { - var ( - hyphaData, isOld = GetHyphaData(hyphaName) - fullPath = filepath.Join(WikiDir, hyphaName+ext) - originalFullPath = &hyphaData.TextPath - ) - if hop.Type == history.TypeEditBinary { - originalFullPath = &hyphaData.BinaryPath - } - - if err := os.MkdirAll(filepath.Dir(fullPath), 0777); err != nil { - return hop.WithError(err) - } - - if err := ioutil.WriteFile(fullPath, data, 0644); err != nil { - return hop.WithError(err) - } - - if isOld && *originalFullPath != fullPath && *originalFullPath != "" { - if err := history.Rename(*originalFullPath, fullPath); err != nil { - return hop.WithError(err) - } - log.Println("Move", *originalFullPath, "to", fullPath) - } - // New hyphae must be added to the hypha storage - if !isOld { - HyphaStorage[hyphaName] = hyphaData - hyphae.IncrementCount() - } - *originalFullPath = fullPath - if isOld && hop.Type == history.TypeEditText && !history.FileChanged(fullPath) { - return hop.Abort() - } - return hop.WithFiles(fullPath). - WithUser(u). - Apply() -} - -// UploadText loads a new text part from `textData` for hypha `hyphaName`. -func UploadText(hyphaName, textData string, u *user.User) *history.HistoryOp { - return uploadHelp( - history. - Operation(history.TypeEditText). - WithMsg(fmt.Sprintf("Edit ‘%s’", hyphaName)), - hyphaName, ".myco", []byte(textData), u) -} - -// UploadBinary loads a new binary part from `file` for hypha `hyphaName` with `hd`. The contents have the specified `mime` type. It must be marked if the hypha `isOld`. -func UploadBinary(hyphaName, mime string, file multipart.File, u *user.User) *history.HistoryOp { - var ( - hop = history.Operation(history.TypeEditBinary).WithMsg(fmt.Sprintf("Upload binary part for ‘%s’ with type ‘%s’", hyphaName, mime)) - data, err = ioutil.ReadAll(file) - ) - if err != nil { - return hop.WithError(err).Apply() - } - return uploadHelp(hop, hyphaName, mimetype.ToExtension(mime), data, u) -} - -// DeleteHypha deletes hypha and makes a history record about that. -func (hd *HyphaData) DeleteHypha(hyphaName string, u *user.User) *history.HistoryOp { - hop := history.Operation(history.TypeDeleteHypha). - WithFilesRemoved(hd.TextPath, hd.BinaryPath). - WithMsg(fmt.Sprintf("Delete ‘%s’", hyphaName)). - WithUser(u). - Apply() - if len(hop.Errs) == 0 { - delete(HyphaStorage, hyphaName) - hyphae.DecrementCount() - } - return hop -} - -// UnattachHypha unattaches hypha and makes a history record about that. -func (hd *HyphaData) UnattachHypha(hyphaName string, u *user.User) *history.HistoryOp { - hop := history.Operation(history.TypeUnattachHypha). - WithFilesRemoved(hd.BinaryPath). - WithMsg(fmt.Sprintf("Unattach ‘%s’", hyphaName)). - WithUser(u). - Apply() - if len(hop.Errs) == 0 { - hd, ok := HyphaStorage[hyphaName] - if ok { - if hd.BinaryPath != "" { - hd.BinaryPath = "" - } - // If nothing is left of the hypha - if hd.TextPath == "" { - delete(HyphaStorage, hyphaName) - hyphae.DecrementCount() - } - } - } - return hop -} - -func findHyphaeToRename(hyphaName string, recursive bool) []string { - hyphae := []string{hyphaName} - if recursive { - hyphae = append(hyphae, util.FindSubhyphae(hyphaName, IterateHyphaNamesWith)...) - } - return hyphae -} - -func renamingPairs(hyphaNames []string, replaceName func(string) string) (map[string]string, error) { - renameMap := make(map[string]string) - for _, hn := range hyphaNames { - if hd, ok := HyphaStorage[hn]; ok { - if _, nameUsed := HyphaStorage[replaceName(hn)]; nameUsed { - return nil, errors.New("Hypha " + replaceName(hn) + " already exists") - } - if hd.TextPath != "" { - renameMap[hd.TextPath] = replaceName(hd.TextPath) - } - if hd.BinaryPath != "" { - renameMap[hd.BinaryPath] = replaceName(hd.BinaryPath) - } - } - } - return renameMap, nil -} - -// word Data is plural here -func relocateHyphaData(hyphaNames []string, replaceName func(string) string) { - for _, hyphaName := range hyphaNames { - if hd, ok := HyphaStorage[hyphaName]; ok { - hd.TextPath = replaceName(hd.TextPath) - hd.BinaryPath = replaceName(hd.BinaryPath) - HyphaStorage[replaceName(hyphaName)] = hd - delete(HyphaStorage, hyphaName) - } - } -} - -// RenameHypha renames hypha from old name `hyphaName` to `newName` and makes a history record about that. If `recursive` is `true`, its subhyphae will be renamed the same way. -func RenameHypha(hyphaName, newName string, recursive bool, u *user.User) *history.HistoryOp { - var ( - re = regexp.MustCompile(`(?i)` + hyphaName) - replaceName = func(str string) string { - return re.ReplaceAllString(CanonicalName(str), newName) - } - hyphaNames = findHyphaeToRename(hyphaName, recursive) - renameMap, err = renamingPairs(hyphaNames, replaceName) - renameMsg = "Rename ‘%s’ to ‘%s’" - hop = history.Operation(history.TypeRenameHypha) - ) - if err != nil { - hop.Errs = append(hop.Errs, err) - return hop - } - if recursive { - renameMsg += " recursively" - } - hop.WithFilesRenamed(renameMap). - WithMsg(fmt.Sprintf(renameMsg, hyphaName, newName)). - WithUser(u). - Apply() - if len(hop.Errs) == 0 { - relocateHyphaData(hyphaNames, replaceName) - } - return hop -} - -// binaryHtmlBlock creates an html block for binary part of the hypha. -func binaryHtmlBlock(hyphaName string, hd *HyphaData) string { - switch filepath.Ext(hd.BinaryPath) { - case ".jpg", ".gif", ".png", ".webp", ".svg", ".ico": - return fmt.Sprintf(` - `, hyphaName) - case ".ogg", ".webm", ".mp4": - return fmt.Sprintf(` -This hypha's media cannot be rendered. Download it
-^?!:#@><*|\"\\'&%
"), "Invalid name"
+ }
+
+ return nil, ""
+}
+
+// RenameHypha renames hypha from old name `hyphaName` to `newName` and makes a history record about that. If `recursive` is `true`, its subhyphae will be renamed the same way.
+func (h *Hypha) RenameHypha(newHypha *Hypha, recursive bool, u *user.User) (hop *history.HistoryOp, errtitle string) {
+ h.Lock()
+ defer h.Unlock()
+ newHypha.Lock()
+ defer newHypha.Unlock()
+ hop = history.Operation(history.TypeRenameHypha)
+
+ if err, errtitle := h.CanRename(u); errtitle != "" {
+ hop.WithError(err)
+ return hop, errtitle
+ }
+ if err, errtitle := canRenameThisToThat(h, newHypha, u); errtitle != "" {
+ hop.WithError(err)
+ return hop, errtitle
+ }
+
+ var (
+ re = regexp.MustCompile(`(?i)` + h.Name)
+ replaceName = func(str string) string {
+ return re.ReplaceAllString(util.CanonicalName(str), newHypha.Name)
+ }
+ hyphaeToRename = findHyphaeToRename(h, recursive)
+ renameMap, err = renamingPairs(hyphaeToRename, replaceName)
+ renameMsg = "Rename ‘%s’ to ‘%s’"
+ )
+ if err != nil {
+ hop.Errs = append(hop.Errs, err)
+ return hop, hop.FirstErrorText()
+ }
+ if recursive && len(hyphaeToRename) > 0 {
+ renameMsg += " recursively"
+ }
+ hop.WithFilesRenamed(renameMap).
+ WithMsg(fmt.Sprintf(renameMsg, h.Name, newHypha.Name)).
+ WithUser(u).
+ Apply()
+ if len(hop.Errs) == 0 {
+ for _, h := range hyphaeToRename {
+ h.renameTo(replaceName(h.Name))
+ }
+ }
+ return hop, ""
+}
+
+func findHyphaeToRename(superhypha *Hypha, recursive bool) []*Hypha {
+ hyphae := []*Hypha{superhypha}
+ if recursive {
+ hyphae = append(hyphae, superhypha.Subhyphae()...)
+ }
+ return hyphae
+}
+
+func renamingPairs(hyphaeToRename []*Hypha, replaceName func(string) string) (map[string]string, error) {
+ renameMap := make(map[string]string)
+ newNames := make([]string, len(hyphaeToRename))
+ for _, h := range hyphaeToRename {
+ h.RLock()
+ newNames = append(newNames, replaceName(h.Name))
+ if h.TextPath != "" {
+ renameMap[h.TextPath] = replaceName(h.TextPath)
+ }
+ if h.BinaryPath != "" {
+ renameMap[h.BinaryPath] = replaceName(h.BinaryPath)
+ }
+ h.RUnlock()
+ }
+ if firstFailure, ok := AreFreeNames(newNames...); !ok {
+ return nil, errors.New("Hypha " + firstFailure + " already exists")
+ }
+ return renameMap, nil
+}
diff --git a/hyphae/unattach.go b/hyphae/unattach.go
new file mode 100644
index 0000000..dab8dce
--- /dev/null
+++ b/hyphae/unattach.go
@@ -0,0 +1,66 @@
+package hyphae
+
+import (
+ "errors"
+ "fmt"
+ "log"
+
+ "github.com/bouncepaw/mycorrhiza/history"
+ "github.com/bouncepaw/mycorrhiza/user"
+)
+
+func rejectUnattachLog(h *Hypha, u *user.User, errmsg string) {
+ log.Printf("Reject unattach ‘%s’ by @%s: %s\n", h.Name, u.Name, errmsg)
+}
+
+// CanUnattach checks if given user can unattach given hypha. If they can, `errtitle` is an empty string and `err` is nil. If they cannot, `errtitle` is not an empty string, and `err` is an error.
+func (h *Hypha) CanUnattach(u *user.User) (err error, errtitle string) {
+ if !u.CanProceed("unattach-confirm") {
+ rejectUnattachLog(h, u, "no rights")
+ return errors.New("Not enough rights to unattach, you must be a trusted editor"), "Not enough rights"
+ }
+
+ if !h.Exists {
+ rejectUnattachLog(h, u, "does not exist")
+ return errors.New("Cannot unattach this hypha because it does not exist"), "Does not exist"
+ }
+
+ if h.BinaryPath == "" {
+ rejectUnattachLog(h, u, "no amnt")
+ return errors.New("Cannot unattach this hypha because it has no attachment"), "No attachment"
+ }
+
+ return nil, ""
+}
+
+// UnattachHypha unattaches hypha and makes a history record about that.
+func (h *Hypha) UnattachHypha(u *user.User) (hop *history.HistoryOp, errtitle string) {
+ h.Lock()
+ defer h.Unlock()
+ hop = history.Operation(history.TypeUnattachHypha)
+
+ if err, errtitle := h.CanUnattach(u); errtitle != "" {
+ hop.WithError(err)
+ return hop, errtitle
+ }
+
+ hop.
+ WithFilesRemoved(h.BinaryPath).
+ WithMsg(fmt.Sprintf("Unattach ‘%s’", h.Name)).
+ WithUser(u).
+ Apply()
+
+ if len(hop.Errs) > 0 {
+ rejectUnattachLog(h, u, "fail")
+ return hop.WithError(errors.New(fmt.Sprintf("Could not unattach this hypha due to internal server errors: %v
", hop.Errs))), "Error"
+ }
+
+ if h.BinaryPath != "" {
+ h.BinaryPath = ""
+ }
+ // If nothing is left of the hypha
+ if h.TextPath == "" {
+ h.delete()
+ }
+ return hop, ""
+}
diff --git a/hyphae/upload.go b/hyphae/upload.go
new file mode 100644
index 0000000..c66d942
--- /dev/null
+++ b/hyphae/upload.go
@@ -0,0 +1,122 @@
+package hyphae
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "mime/multipart"
+ "os"
+ "path/filepath"
+
+ "github.com/bouncepaw/mycorrhiza/history"
+ "github.com/bouncepaw/mycorrhiza/mimetype"
+ "github.com/bouncepaw/mycorrhiza/user"
+ "github.com/bouncepaw/mycorrhiza/util"
+)
+
+func rejectEditLog(h *Hypha, u *user.User, errmsg string) {
+ log.Printf("Reject edit ‘%s’ by @%s: %s\n", h.Name, u.Name, errmsg)
+}
+
+func rejectAttachLog(h *Hypha, u *user.User, errmsg string) {
+ log.Printf("Reject attach ‘%s’ by @%s: %s\n", h.Name, u.Name, errmsg)
+}
+
+func (h *Hypha) CanEdit(u *user.User) (err error, errtitle string) {
+ if !u.CanProceed("edit") {
+ rejectEditLog(h, u, "no rights")
+ return errors.New("You must be an editor to edit pages."), "Not enough rights"
+ }
+ return nil, ""
+}
+
+func (h *Hypha) CanUploadThat(data []byte, u *user.User) (err error, errtitle string) {
+ if len(data) == 0 {
+ return errors.New("No text data passed"), "Empty"
+ }
+ return nil, ""
+}
+
+func (h *Hypha) UploadText(textData []byte, u *user.User) (hop *history.HistoryOp, errtitle string) {
+ hop = history.Operation(history.TypeEditText)
+ if h.Exists {
+ hop.WithMsg(fmt.Sprintf("Edit ‘%s’", h.Name))
+ } else {
+ hop.WithMsg(fmt.Sprintf("Create ‘%s’", h.Name))
+ }
+
+ if err, errtitle := h.CanEdit(u); err != nil {
+ return hop.WithError(err), errtitle
+ }
+ if err, errtitle := h.CanUploadThat(textData, u); err != nil {
+ return hop.WithError(err), errtitle
+ }
+
+ return h.uploadHelp(hop, ".myco", textData, u)
+}
+
+func (h *Hypha) CanAttach(err error, u *user.User) (error, string) {
+ if !u.CanProceed("upload-binary") {
+ rejectAttachLog(h, u, "no rights")
+ return errors.New("You must be an editor to upload attachments."), "Not enough rights"
+ }
+
+ if err != nil {
+ rejectAttachLog(h, u, err.Error())
+ return errors.New("No binary data passed"), err.Error()
+ }
+ return nil, ""
+}
+
+func (h *Hypha) UploadBinary(mime string, file multipart.File, u *user.User) (*history.HistoryOp, string) {
+ var (
+ hop = history.Operation(history.TypeEditBinary).WithMsg(fmt.Sprintf("Upload binary part for ‘%s’ with type ‘%s’", h.Name, mime))
+ data, err = ioutil.ReadAll(file)
+ )
+
+ if err != nil {
+ return hop.WithError(err), err.Error()
+ }
+ if err, errtitle := h.CanEdit(u); err != nil {
+ return hop.WithError(err), errtitle
+ }
+ if err, errtitle := h.CanUploadThat(data, u); err != nil {
+ return hop.WithError(err), errtitle
+ }
+
+ return h.uploadHelp(hop, mimetype.ToExtension(mime), data, u)
+}
+
+// uploadHelp is a helper function for UploadText and UploadBinary
+func (h *Hypha) uploadHelp(hop *history.HistoryOp, ext string, data []byte, u *user.User) (*history.HistoryOp, string) {
+ var (
+ fullPath = filepath.Join(util.WikiDir, h.Name+ext)
+ originalFullPath = &h.TextPath
+ )
+ if hop.Type == history.TypeEditBinary {
+ originalFullPath = &h.BinaryPath
+ }
+
+ if err := os.MkdirAll(filepath.Dir(fullPath), 0777); err != nil {
+ return hop.WithError(err), err.Error()
+ }
+
+ if err := ioutil.WriteFile(fullPath, data, 0644); err != nil {
+ return hop.WithError(err), err.Error()
+ }
+
+ if h.Exists && *originalFullPath != fullPath && *originalFullPath != "" {
+ if err := history.Rename(*originalFullPath, fullPath); err != nil {
+ return hop.WithError(err), err.Error()
+ }
+ log.Println("Move", *originalFullPath, "to", fullPath)
+ }
+
+ h.InsertIfNew()
+ *originalFullPath = fullPath
+ if h.Exists && hop.Type == history.TypeEditText && !history.FileChanged(fullPath) {
+ return hop.Abort(), "No changes"
+ }
+ return hop.WithFiles(fullPath).WithUser(u).Apply(), ""
+}
diff --git a/hyphae/view.go b/hyphae/view.go
new file mode 100644
index 0000000..e53ab4f
--- /dev/null
+++ b/hyphae/view.go
@@ -0,0 +1,72 @@
+package hyphae
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "github.com/bouncepaw/mycorrhiza/markup"
+ "github.com/bouncepaw/mycorrhiza/util"
+)
+
+// FetchTextPart tries to read text file of the given hypha. If there is no file, empty string is returned.
+func (h *Hypha) FetchTextPart() (string, error) {
+ if h.TextPath == "" {
+ return "", nil
+ }
+ text, err := ioutil.ReadFile(h.TextPath)
+ if os.IsNotExist(err) {
+ return "", nil
+ } else if err != nil {
+ return "", err
+ }
+ return string(text), nil
+}
+
+// binaryHtmlBlock creates an html block for binary part of the hypha.
+func (h *Hypha) BinaryHtmlBlock() string {
+ switch filepath.Ext(h.BinaryPath) {
+ case ".jpg", ".gif", ".png", ".webp", ".svg", ".ico":
+ return fmt.Sprintf(`
+ `, h.Name)
+ case ".ogg", ".webm", ".mp4":
+ return fmt.Sprintf(`
+