diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/Makefile b/Makefile
index dd9d604..5f2d62e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,9 @@
run: build
./mycorrhiza metarrhiza
+run_with_fixed_auth: build
+ ./mycorrhiza -auth-method fixed metarrhiza
+
build:
go generate
go build .
diff --git a/README.md b/README.md
index 21b4503..82876c6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# š MycorrhizaWiki 0.10
+# š MycorrhizaWiki 0.11
A wiki engine.
## Building
@@ -15,13 +15,21 @@ make
```
mycorrhiza [OPTIONS...] WIKI_PATH
+WIKI_PATH must be a path to git repository which you want to be a wiki.
+
Options:
+ -auth-method string
+ What auth method to use. Variants: "none", "fixed" (default "none")
+ -fixed-credentials-path string
+ Used when -auth-method=fixed. Path to file with user credentials. (default "mycocredentials.json")
-home string
The home page (default "home")
-port string
Port to serve the wiki at (default "1737")
-title string
How to call your wiki in the navititle (default "š")
+ -user-tree string
+ Hypha which is a superhypha of all user pages (default "u")
```
## Features
@@ -39,11 +47,11 @@ Options:
* Hyphae can be deleted (while still preserving history)
* Hyphae can be renamed (recursive renaming of subhyphae is also supported)
* Light on resources: I run a home wiki on this engine 24/7 at an [Orange Ļ Lite](http://www.orangepi.org/orangepilite/).
+* Authorization with pre-set credentials
## Contributing
Help is always needed. We have a [tg chat](https://t.me/mycorrhizadev) where some development is coordinated. Feel free to open an issue or contact me.
## Future plans
* Tagging system
-* Authorization
* Better history viewing
diff --git a/flag.go b/flag.go
index d5d6df4..8a9857a 100644
--- a/flag.go
+++ b/flag.go
@@ -5,6 +5,7 @@ import (
"log"
"path/filepath"
+ "github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
)
@@ -12,6 +13,9 @@ func init() {
flag.StringVar(&util.ServerPort, "port", "1737", "Port to serve the wiki at")
flag.StringVar(&util.HomePage, "home", "home", "The home page")
flag.StringVar(&util.SiteTitle, "title", "š", "How to call your wiki in the navititle")
+ flag.StringVar(&util.UserTree, "user-tree", "u", "Hypha which is a superhypha of all user pages")
+ flag.StringVar(&util.AuthMethod, "auth-method", "none", "What auth method to use. Variants: \"none\", \"fixed\"")
+ flag.StringVar(&util.FixedCredentialsPath, "fixed-credentials-path", "mycocredentials.json", "Used when -auth-method=fixed. Path to file with user credentials.")
}
// Do the things related to cli args and die maybe
@@ -33,4 +37,17 @@ func parseCliArgs() {
if !isCanonicalName(util.HomePage) {
log.Fatal("Error: you must use a proper name for the homepage")
}
+
+ if !isCanonicalName(util.UserTree) {
+ log.Fatal("Error: you must use a proper name for user tree")
+ }
+
+ switch util.AuthMethod {
+ case "none":
+ case "fixed":
+ user.AuthUsed = true
+ user.PopulateFixedUserStorage()
+ default:
+ log.Fatal("Error: unknown auth method:", util.AuthMethod)
+ }
}
diff --git a/go.mod b/go.mod
index e2ffb7c..995478b 100644
--- a/go.mod
+++ b/go.mod
@@ -2,4 +2,7 @@ module github.com/bouncepaw/mycorrhiza
go 1.14
-require github.com/valyala/quicktemplate v1.6.3
+require (
+ github.com/adrg/xdg v0.2.2
+ github.com/valyala/quicktemplate v1.6.3
+)
diff --git a/go.sum b/go.sum
index a9a43a1..dbbc51b 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,12 @@
+github.com/adrg/xdg v0.2.2 h1:A7ZHKRz5KGOLJX/bg7IPzStryhvCzAE1wX+KWawPiAo=
+github.com/adrg/xdg v0.2.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
@@ -13,3 +19,5 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/history/history.go b/history/history.go
index ec409c2..247b4c5 100644
--- a/history/history.go
+++ b/history/history.go
@@ -9,6 +9,7 @@ import (
"strings"
"time"
+ "github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
)
@@ -76,11 +77,19 @@ func (rev Revision) HyphaeLinks() (html string) {
}
func (rev Revision) RecentChangesEntry() (html string) {
+ if user.AuthUsed && rev.Username != "anon" {
+ return fmt.Sprintf(`
+
%[1]s
+%[2]s
+%[5]s
+%[6]s by %[4]s
+`, rev.TimeString(), rev.Hash, util.UserTree, rev.Username, rev.HyphaeLinks(), rev.Message)
+ }
return fmt.Sprintf(`
-%s
-%s
-%s
-%s
+%[1]s
+%[2]s
+%[3]s
+%[4]s
`, rev.TimeString(), rev.Hash, rev.HyphaeLinks(), rev.Message)
}
diff --git a/history/information.go b/history/information.go
index 164e3a9..bc65cac 100644
--- a/history/information.go
+++ b/history/information.go
@@ -15,7 +15,7 @@ func RecentChanges(n int) string {
var (
out, err = gitsh(
"log", "--oneline", "--no-merges",
- "--pretty=format:\"%h\t%ce\t%ct\t%s\"",
+ "--pretty=format:\"%h\t%ae\t%at\t%s\"",
"--max-count="+strconv.Itoa(n),
)
revs []Revision
diff --git a/history/operations.go b/history/operations.go
index 0507483..e16c367 100644
--- a/history/operations.go
+++ b/history/operations.go
@@ -8,6 +8,7 @@ import (
"path/filepath"
"sync"
+ "github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
)
@@ -29,7 +30,7 @@ const (
type HistoryOp struct {
// All errors are appended here.
Errs []error
- opType OpType
+ Type OpType
userMsg string
name string
email string
@@ -39,8 +40,10 @@ type HistoryOp struct {
func Operation(opType OpType) *HistoryOp {
gitMutex.Lock()
hop := &HistoryOp{
- Errs: []error{},
- opType: opType,
+ Errs: []error{},
+ name: "anon",
+ email: "anon@mycorrhiza",
+ Type: opType,
}
return hop
}
@@ -116,9 +119,11 @@ func (hop *HistoryOp) WithMsg(userMsg string) *HistoryOp {
return hop
}
-// WithSignature sets a signature for the future commit. You need to pass a username only, the rest is upon us (including email and time).
-func (hop *HistoryOp) WithSignature(username string) *HistoryOp {
- hop.name = username
- hop.email = username + "@mycorrhiza" // A fake email, why not
+// WithUser sets a user for the commit.
+func (hop *HistoryOp) WithUser(u *user.User) *HistoryOp {
+ if u.Group != user.UserAnon {
+ hop.name = u.Name
+ hop.email = u.Name + "@mycorrhiza"
+ }
return hop
}
diff --git a/http_auth.go b/http_auth.go
new file mode 100644
index 0000000..2e1039b
--- /dev/null
+++ b/http_auth.go
@@ -0,0 +1,62 @@
+package main
+
+import (
+ "log"
+ "net/http"
+
+ "github.com/bouncepaw/mycorrhiza/templates"
+ "github.com/bouncepaw/mycorrhiza/user"
+)
+
+func init() {
+ http.HandleFunc("/login", handlerLogin)
+ http.HandleFunc("/login-data", handlerLoginData)
+ http.HandleFunc("/logout", handlerLogout)
+ http.HandleFunc("/logout-confirm", handlerLogoutConfirm)
+}
+
+func handlerLogout(w http.ResponseWriter, rq *http.Request) {
+ var (
+ u = user.FromRequest(rq)
+ can = u != nil
+ )
+ w.Header().Set("Content-Type", "text/html;charset=utf-8")
+ if can {
+ log.Println("User", u.Name, "tries to log out")
+ w.WriteHeader(http.StatusOK)
+ } else {
+ log.Println("Unknown user tries to log out")
+ w.WriteHeader(http.StatusForbidden)
+ }
+ w.Write([]byte(base("Logout?", templates.LogoutHTML(can))))
+}
+
+func handlerLogoutConfirm(w http.ResponseWriter, rq *http.Request) {
+ user.LogoutFromRequest(w, rq)
+ http.Redirect(w, rq, "/", http.StatusSeeOther)
+}
+
+func handlerLoginData(w http.ResponseWriter, rq *http.Request) {
+ log.Println(rq.URL)
+ var (
+ username = CanonicalName(rq.PostFormValue("username"))
+ password = rq.PostFormValue("password")
+ err = user.LoginDataHTTP(w, rq, username, password)
+ )
+ if err != "" {
+ w.Write([]byte(base(err, templates.LoginErrorHTML(err))))
+ } else {
+ http.Redirect(w, rq, "/", http.StatusSeeOther)
+ }
+}
+
+func handlerLogin(w http.ResponseWriter, rq *http.Request) {
+ log.Println(rq.URL)
+ w.Header().Set("Content-Type", "text/html;charset=utf-8")
+ if user.AuthUsed {
+ w.WriteHeader(http.StatusOK)
+ } else {
+ w.WriteHeader(http.StatusForbidden)
+ }
+ w.Write([]byte(base("Login", templates.LoginHTML())))
+}
diff --git a/http_mutators.go b/http_mutators.go
index beb43e6..232fed3 100644
--- a/http_mutators.go
+++ b/http_mutators.go
@@ -6,16 +6,19 @@ import (
"net/http"
"github.com/bouncepaw/mycorrhiza/templates"
+ "github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
)
func init() {
- http.HandleFunc("/upload-binary/", handlerUploadBinary)
- http.HandleFunc("/upload-text/", handlerUploadText)
+ // Those that do not actually mutate anything:
http.HandleFunc("/edit/", handlerEdit)
http.HandleFunc("/delete-ask/", handlerDeleteAsk)
- http.HandleFunc("/delete-confirm/", handlerDeleteConfirm)
http.HandleFunc("/rename-ask/", handlerRenameAsk)
+ // And those that do mutate something:
+ http.HandleFunc("/upload-binary/", handlerUploadBinary)
+ http.HandleFunc("/upload-text/", handlerUploadText)
+ http.HandleFunc("/delete-confirm/", handlerDeleteConfirm)
http.HandleFunc("/rename-confirm/", handlerRenameConfirm)
}
@@ -25,22 +28,28 @@ func handlerRenameAsk(w http.ResponseWriter, rq *http.Request) {
hyphaName = HyphaNameFromRq(rq, "rename-ask")
_, isOld = HyphaStorage[hyphaName]
)
- util.HTTP200Page(w, base("Rename "+hyphaName+"?", templates.RenameAskHTML(hyphaName, isOld)))
+ if ok := user.CanProceed(rq, "rename-confirm"); !ok {
+ 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)))
}
func handlerRenameConfirm(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
hyphaName = HyphaNameFromRq(rq, "rename-confirm")
- hyphaData, isOld = HyphaStorage[hyphaName]
+ _, isOld = HyphaStorage[hyphaName]
newName = CanonicalName(rq.PostFormValue("new-name"))
_, newNameIsUsed = HyphaStorage[newName]
- recursive bool
+ recursive = rq.PostFormValue("recursive") == "true"
+ u = user.FromRequest(rq).OrAnon()
)
- if rq.PostFormValue("recursive") == "true" {
- recursive = true
- }
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))
@@ -54,12 +63,12 @@ func handlerRenameConfirm(w http.ResponseWriter, rq *http.Request) {
HttpErr(w, http.StatusBadRequest, hyphaName, "Error: invalid name",
"Invalid new name. Names cannot contain characters ^?!:#@><*|\"\\'&%
")
default:
- if hop := hyphaData.RenameHypha(hyphaName, newName, recursive); len(hop.Errs) == 0 {
- http.Redirect(w, rq, "/page/"+newName, http.StatusSeeOther)
- } else {
+ 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)
}
}
}
@@ -71,7 +80,12 @@ func handlerDeleteAsk(w http.ResponseWriter, rq *http.Request) {
hyphaName = HyphaNameFromRq(rq, "delete-ask")
_, isOld = HyphaStorage[hyphaName]
)
- util.HTTP200Page(w, base("Delete "+hyphaName+"?", templates.DeleteAskHTML(hyphaName, isOld)))
+ if ok := user.CanProceed(rq, "delete-ask"); !ok {
+ HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a moderator to delete pages.")
+ log.Println("Rejected", rq.URL)
+ return
+ }
+ util.HTTP200Page(w, base("Delete "+hyphaName+"?", templates.DeleteAskHTML(rq, hyphaName, isOld)))
}
// handlerDeleteConfirm deletes a hypha for sure
@@ -80,22 +94,27 @@ func handlerDeleteConfirm(w http.ResponseWriter, rq *http.Request) {
var (
hyphaName = HyphaNameFromRq(rq, "delete-confirm")
hyphaData, isOld = HyphaStorage[hyphaName]
+ u = user.FromRequest(rq)
)
- if isOld {
- // If deleted successfully
- if hop := hyphaData.DeleteHypha(hyphaName); len(hop.Errs) == 0 {
- http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
- } else {
- HttpErr(w, http.StatusInternalServerError, hyphaName,
- "Error: could not delete hypha",
- fmt.Sprintf("Could not delete this hypha due to an internal error. Server errors: %v
", hop.Errs))
- }
- } else {
+ 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)
}
// handlerEdit shows the edit form. It doesn't edit anything actually.
@@ -108,6 +127,11 @@ func handlerEdit(w http.ResponseWriter, rq *http.Request) {
textAreaFill string
err error
)
+ if ok := user.CanProceed(rq, "edit"); !ok {
+ HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.")
+ log.Println("Rejected", rq.URL)
+ return
+ }
if isOld {
textAreaFill, err = FetchTextPart(hyphaData)
if err != nil {
@@ -118,25 +142,27 @@ func handlerEdit(w http.ResponseWriter, rq *http.Request) {
} else {
warning = `You are creating a new hypha.
`
}
- util.HTTP200Page(w, base("Edit "+hyphaName, templates.EditHTML(hyphaName, textAreaFill, warning)))
+ util.HTTP200Page(w, base("Edit "+hyphaName, templates.EditHTML(rq, hyphaName, textAreaFill, warning)))
}
// handlerUploadText uploads a new text part for the hypha.
func handlerUploadText(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
- hyphaName = HyphaNameFromRq(rq, "upload-text")
- hyphaData, isOld = HyphaStorage[hyphaName]
- textData = rq.PostFormValue("text")
+ hyphaName = HyphaNameFromRq(rq, "upload-text")
+ textData = rq.PostFormValue("text")
+ u = user.FromRequest(rq).OrAnon()
)
- if !isOld {
- hyphaData = &HyphaData{}
+ if ok := user.CanProceed(rq, "upload-text"); !ok {
+ 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")
return
}
- if hop := hyphaData.UploadText(hyphaName, textData, isOld); len(hop.Errs) != 0 {
+ if hop := UploadText(hyphaName, textData, u); len(hop.Errs) != 0 {
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error())
} else {
http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
@@ -146,31 +172,37 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) {
// handlerUploadBinary uploads a new binary part for the hypha.
func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
- hyphaName := HyphaNameFromRq(rq, "upload-binary")
- rq.ParseMultipartForm(10 << 20)
+ 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()
}
+
// If file is not passed:
if err != nil {
HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No binary data passed")
return
}
+
// If file is passed:
var (
- hyphaData, isOld = HyphaStorage[hyphaName]
- mime = handler.Header.Get("Content-Type")
+ mime = handler.Header.Get("Content-Type")
+ hop = UploadBinary(hyphaName, mime, file, u)
)
- if !isOld {
- hyphaData = &HyphaData{}
- }
- hop := hyphaData.UploadBinary(hyphaName, mime, file, isOld)
if len(hop.Errs) != 0 {
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error())
- } else {
- http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
+ return
}
+ http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
}
diff --git a/http_readers.go b/http_readers.go
index 7870d3e..b8887e9 100644
--- a/http_readers.go
+++ b/http_readers.go
@@ -40,6 +40,7 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) {
contents = markup.ToHtml(hyphaName, textContents)
}
page := templates.RevisionHTML(
+ rq,
hyphaName,
naviTitle(hyphaName),
contents,
@@ -67,7 +68,7 @@ func handlerHistory(w http.ResponseWriter, rq *http.Request) {
log.Println("Found", len(revs), "revisions for", hyphaName)
util.HTTP200Page(w,
- base(hyphaName, templates.HistoryHTML(hyphaName, tbody)))
+ base(hyphaName, templates.HistoryHTML(rq, hyphaName, tbody)))
}
// handlerText serves raw source text of the hypha.
@@ -110,7 +111,7 @@ func handlerPage(w http.ResponseWriter, rq *http.Request) {
contents = binaryHtmlBlock(hyphaName, data) + contents
}
}
- util.HTTP200Page(w, base(hyphaName, templates.PageHTML(hyphaName,
+ util.HTTP200Page(w, base(hyphaName, templates.PageHTML(rq, hyphaName,
naviTitle(hyphaName),
contents,
tree.TreeAsHtml(hyphaName, IterateHyphaNamesWith))))
diff --git a/hypha.go b/hypha.go
index a49b785..e412c14 100644
--- a/hypha.go
+++ b/hypha.go
@@ -12,6 +12,7 @@ import (
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/markup"
+ "github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
)
@@ -31,6 +32,16 @@ func init() {
}
return
}
+ markup.HyphaIterate = IterateHyphaNamesWith
+}
+
+// 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.
@@ -40,10 +51,16 @@ type HyphaData struct {
}
// uploadHelp is a helper function for UploadText and UploadBinary
-func (hd *HyphaData) uploadHelp(hop *history.HistoryOp, hyphaName, ext string, originalFullPath *string, isOld bool, data []byte) *history.HistoryOp {
+func uploadHelp(hop *history.HistoryOp, hyphaName, ext string, data []byte, u *user.User) *history.HistoryOp {
var (
- fullPath = filepath.Join(WikiDir, hyphaName+ext)
+ 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)
}
@@ -51,30 +68,34 @@ func (hd *HyphaData) uploadHelp(hop *history.HistoryOp, hyphaName, ext string, o
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] = hd
+ HyphaStorage[hyphaName] = hyphaData
}
*originalFullPath = fullPath
- log.Printf("%v\n", *hd)
return hop.WithFiles(fullPath).
- WithSignature("anon").
+ WithUser(u).
Apply()
}
-// UploadText loads a new text part from `textData` for hypha `hyphaName` with `hd`. It must be marked if the hypha `isOld`.
-func (hd *HyphaData) UploadText(hyphaName, textData string, isOld bool) *history.HistoryOp {
- hop := history.Operation(history.TypeEditText).WithMsg(fmt.Sprintf("Edit ā%sā", hyphaName))
- return hd.uploadHelp(hop, hyphaName, ".myco", &hd.textPath, isOld, []byte(textData))
+// 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 (hd *HyphaData) UploadBinary(hyphaName, mime string, file multipart.File, isOld bool) *history.HistoryOp {
+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)
@@ -82,16 +103,15 @@ func (hd *HyphaData) UploadBinary(hyphaName, mime string, file multipart.File, i
if err != nil {
return hop.WithError(err).Apply()
}
-
- return hd.uploadHelp(hop, hyphaName, MimeToExtension(mime), &hd.binaryPath, isOld, data)
+ return uploadHelp(hop, hyphaName, MimeToExtension(mime), data, u)
}
// DeleteHypha deletes hypha and makes a history record about that.
-func (hd *HyphaData) DeleteHypha(hyphaName string) *history.HistoryOp {
+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)).
- WithSignature("anon").
+ WithUser(u).
Apply()
if len(hop.Errs) == 0 {
delete(HyphaStorage, hyphaName)
@@ -138,7 +158,7 @@ func relocateHyphaData(hyphaNames []string, replaceName func(string) string) {
}
// 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 (hd *HyphaData) RenameHypha(hyphaName, newName string, recursive bool) *history.HistoryOp {
+func RenameHypha(hyphaName, newName string, recursive bool, u *user.User) *history.HistoryOp {
var (
replaceName = func(str string) string {
return strings.Replace(str, hyphaName, newName, 1)
@@ -157,7 +177,7 @@ func (hd *HyphaData) RenameHypha(hyphaName, newName string, recursive bool) *his
}
hop.WithFilesRenamed(renameMap).
WithMsg(fmt.Sprintf(renameMsg, hyphaName, newName)).
- WithSignature("anon").
+ WithUser(u).
Apply()
if len(hop.Errs) == 0 {
relocateHyphaData(hyphaNames, replaceName)
@@ -171,7 +191,7 @@ func binaryHtmlBlock(hyphaName string, hd *HyphaData) string {
case ".jpg", ".gif", ".png", ".webp", ".svg", ".ico":
return fmt.Sprintf(`
-
+
`, hyphaName)
case ".ogg", ".webm", ".mp4":
return fmt.Sprintf(`
diff --git a/main.go b/main.go
index 0f63208..51d7610 100644
--- a/main.go
+++ b/main.go
@@ -15,6 +15,7 @@ import (
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/templates"
+ "github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
)
@@ -22,7 +23,7 @@ import (
var WikiDir string
// HyphaPattern is a pattern which all hyphae must match.
-var HyphaPattern = regexp.MustCompile(`[^?!:#@><*|"\'&%]+`)
+var HyphaPattern = regexp.MustCompile(`[^?!:#@><*|"\'&%{}]+`)
// HyphaStorage is a mapping between canonical hypha names and their meta information.
var HyphaStorage = make(map[string]*HyphaData)
@@ -63,6 +64,11 @@ var base = templates.BaseHTML
// Reindex all hyphae by checking the wiki storage directory anew.
func handlerReindex(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
+ if ok := user.CanProceed(rq, "reindex"); !ok {
+ HttpErr(w, http.StatusForbidden, util.HomePage, "Not enough rights", "You must be an admin to reindex hyphae.")
+ log.Println("Rejected", rq.URL)
+ return
+ }
HyphaStorage = make(map[string]*HyphaData)
log.Println("Wiki storage directory is", WikiDir)
log.Println("Start indexing hyphae...")
@@ -125,6 +131,7 @@ func main() {
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(WikiDir+"/static"))))
// See http_readers.go for /page/, /text/, /binary/, /history/.
// See http_mutators.go for /upload-binary/, /upload-text/, /edit/, /delete-ask/, /delete-confirm/, /rename-ask/, /rename-confirm/.
+ // See http_auth.go for /login, /login-data, /logout, /logout-confirm
http.HandleFunc("/list", handlerList)
http.HandleFunc("/reindex", handlerReindex)
http.HandleFunc("/random", handlerRandom)
diff --git a/markup/img.go b/markup/img.go
index 244451b..6445ed1 100644
--- a/markup/img.go
+++ b/markup/img.go
@@ -13,53 +13,169 @@ func MatchesImg(line string) bool {
}
type imgEntry struct {
- path string
- sizeH string
- sizeV string
- desc string
+ trimmedPath string
+ path strings.Builder
+ sizeW strings.Builder
+ sizeH strings.Builder
+ desc strings.Builder
}
+func (entry *imgEntry) descriptionAsHtml(hyphaName string) (html string) {
+ if entry.desc.Len() == 0 {
+ return ""
+ }
+ lines := strings.Split(entry.desc.String(), "\n")
+ for _, line := range lines {
+ if line = strings.TrimSpace(line); line != "" {
+ if html != "" {
+ html += ` `
+ }
+ html += ParagraphToHtml(hyphaName, line)
+ }
+ }
+ return `` + html + ` `
+}
+
+func (entry *imgEntry) sizeWAsAttr() string {
+ if entry.sizeW.Len() == 0 {
+ return ""
+ }
+ return ` width="` + entry.sizeW.String() + `"`
+}
+
+func (entry *imgEntry) sizeHAsAttr() string {
+ if entry.sizeH.Len() == 0 {
+ return ""
+ }
+ return ` height="` + entry.sizeH.String() + `"`
+}
+
+type imgState int
+
+const (
+ inRoot imgState = iota
+ inName
+ inDimensionsW
+ inDimensionsH
+ inDescription
+)
+
type Img struct {
entries []imgEntry
- inDesc bool
+ currEntry imgEntry
hyphaName string
+ state imgState
+}
+
+func (img *Img) pushEntry() {
+ if strings.TrimSpace(img.currEntry.path.String()) != "" {
+ img.entries = append(img.entries, img.currEntry)
+ img.currEntry = imgEntry{}
+ img.currEntry.path.Reset()
+ }
}
func (img *Img) Process(line string) (shouldGoBackToNormal bool) {
- if img.inDesc {
- rightBraceIndex := strings.IndexRune(line, '}')
- if cnt := len(img.entries); rightBraceIndex == -1 && cnt != 0 {
- img.entries[cnt-1].desc += "\n" + line
- } else if rightBraceIndex != -1 && cnt != 0 {
- img.entries[cnt-1].desc += "\n" + line[:rightBraceIndex]
- img.inDesc = false
- }
- if strings.Count(line, "}") > 1 {
+ stateToProcessor := map[imgState]func(rune) bool{
+ inRoot: img.processInRoot,
+ inName: img.processInName,
+ inDimensionsW: img.processInDimensionsW,
+ inDimensionsH: img.processInDimensionsH,
+ inDescription: img.processInDescription,
+ }
+ for _, r := range line {
+ if shouldReturnTrue := stateToProcessor[img.state](r); shouldReturnTrue {
return true
}
- } else if s := strings.TrimSpace(line); s != "" {
- if s[0] == '}' {
- return true
- }
- img.parseStartOfEntry(line)
}
return false
}
-func ImgFromFirstLine(line, hyphaName string) Img {
- img := Img{
+func (img *Img) processInDescription(r rune) (shouldReturnTrue bool) {
+ switch r {
+ case '}':
+ img.state = inName
+ default:
+ img.currEntry.desc.WriteRune(r)
+ }
+ return false
+}
+
+func (img *Img) processInRoot(r rune) (shouldReturnTrue bool) {
+ switch r {
+ case '}':
+ img.pushEntry()
+ return true
+ case '\n', '\r':
+ img.pushEntry()
+ case ' ', '\t':
+ default:
+ img.state = inName
+ img.currEntry = imgEntry{}
+ img.currEntry.path.Reset()
+ img.currEntry.path.WriteRune(r)
+ }
+ return false
+}
+
+func (img *Img) processInName(r rune) (shouldReturnTrue bool) {
+ switch r {
+ case '}':
+ img.pushEntry()
+ return true
+ case '|':
+ img.state = inDimensionsW
+ case '{':
+ img.state = inDescription
+ case '\n', '\r':
+ img.pushEntry()
+ img.state = inRoot
+ default:
+ img.currEntry.path.WriteRune(r)
+ }
+ return false
+}
+
+func (img *Img) processInDimensionsW(r rune) (shouldReturnTrue bool) {
+ switch r {
+ case '}':
+ img.pushEntry()
+ return true
+ case '*':
+ img.state = inDimensionsH
+ case ' ', '\t', '\n':
+ case '{':
+ img.state = inDescription
+ default:
+ img.currEntry.sizeW.WriteRune(r)
+ }
+ return false
+}
+
+func (img *Img) processInDimensionsH(r rune) (shouldGoBackToNormal bool) {
+ switch r {
+ case '}':
+ img.pushEntry()
+ return true
+ case ' ', '\t', '\n':
+ case '{':
+ img.state = inDescription
+ default:
+ img.currEntry.sizeH.WriteRune(r)
+ }
+ return false
+}
+
+func ImgFromFirstLine(line, hyphaName string) (img *Img, shouldGoBackToNormal bool) {
+ img = &Img{
hyphaName: hyphaName,
entries: make([]imgEntry, 0),
}
- line = line[strings.IndexRune(line, '{'):]
- if len(line) == 1 { // if { only
- } else {
- line = line[1:] // Drop the {
- }
- return img
+ line = line[strings.IndexRune(line, '{')+1:]
+ return img, img.Process(line)
}
-func (img *Img) canonicalPathFor(path string) string {
+func (img *Img) binaryPathFor(path string) string {
path = strings.TrimSpace(path)
if strings.IndexRune(path, ':') != -1 || strings.IndexRune(path, '/') == 0 {
return path
@@ -68,73 +184,71 @@ func (img *Img) canonicalPathFor(path string) string {
}
}
-func (img *Img) parseStartOfEntry(line string) (entry imgEntry, followedByDesc bool) {
- pipeIndex := strings.IndexRune(line, '|')
- if pipeIndex == -1 { // If no : in string
- entry.path = img.canonicalPathFor(line)
+func (img *Img) pagePathFor(path string) string {
+ path = strings.TrimSpace(path)
+ if strings.IndexRune(path, ':') != -1 || strings.IndexRune(path, '/') == 0 {
+ return path
} else {
- entry.path = img.canonicalPathFor(line[:pipeIndex])
- line = strings.TrimPrefix(line, line[:pipeIndex+1])
-
- var (
- leftBraceIndex = strings.IndexRune(line, '{')
- rightBraceIndex = strings.IndexRune(line, '}')
- dimensions string
- )
-
- if leftBraceIndex == -1 {
- dimensions = line
- } else {
- dimensions = line[:leftBraceIndex]
- }
-
- sizeH, sizeV := parseDimensions(dimensions)
- entry.sizeH = sizeH
- entry.sizeV = sizeV
-
- if leftBraceIndex != -1 && rightBraceIndex == -1 {
- img.inDesc = true
- followedByDesc = true
- entry.desc = strings.TrimPrefix(line, line[:leftBraceIndex+1])
- } else if leftBraceIndex != -1 && rightBraceIndex != -1 {
- entry.desc = line[leftBraceIndex+1 : rightBraceIndex]
- }
+ return "/page/" + xclCanonicalName(img.hyphaName, path)
}
- img.entries = append(img.entries, entry)
- return
}
-func parseDimensions(dimensions string) (sizeH, sizeV string) {
+func parseDimensions(dimensions string) (sizeW, sizeH string) {
xIndex := strings.IndexRune(dimensions, '*')
if xIndex == -1 { // If no x in dimensions
- sizeH = strings.TrimSpace(dimensions)
+ sizeW = strings.TrimSpace(dimensions)
} else {
- sizeH = strings.TrimSpace(dimensions[:xIndex])
- sizeV = strings.TrimSpace(strings.TrimPrefix(dimensions, dimensions[:xIndex+1]))
+ sizeW = strings.TrimSpace(dimensions[:xIndex])
+ sizeH = strings.TrimSpace(strings.TrimPrefix(dimensions, dimensions[:xIndex+1]))
}
return
}
-func (img Img) ToHtml() (html string) {
- for _, entry := range img.entries {
- html += fmt.Sprintf(`
-
-`, entry.path, entry.sizeH, entry.sizeV)
- if entry.desc != "" {
- html += ` `
- for i, line := range strings.Split(entry.desc, "\n") {
- if line != "" {
- if i > 0 {
- html += ` `
- }
- html += ParagraphToHtml(img.hyphaName, line)
- }
- }
- html += ` `
+func (img *Img) checkLinks() map[string]bool {
+ m := make(map[string]bool)
+ for i, entry := range img.entries {
+ // Also trim them for later use
+ entry.trimmedPath = strings.TrimSpace(entry.path.String())
+ isAbsoluteUrl := strings.ContainsRune(entry.trimmedPath, ':')
+ if !isAbsoluteUrl {
+ entry.trimmedPath = canonicalName(entry.trimmedPath)
}
+ img.entries[i] = entry
+ m[entry.trimmedPath] = isAbsoluteUrl
+ }
+ HyphaIterate(func(hyphaName string) {
+ for _, entry := range img.entries {
+ if hyphaName == entry.trimmedPath {
+ m[entry.trimmedPath] = true
+ }
+ }
+ })
+ return m
+}
+
+func (img *Img) ToHtml() (html string) {
+ linkAvailabilityMap := img.checkLinks()
+ isOneImageOnly := len(img.entries) == 1 && img.entries[0].desc.Len() == 0
+ if isOneImageOnly {
+ html += ``
+ } else {
+ html += ``
+ }
+
+ for _, entry := range img.entries {
+ html += ``
+ // If is existing hypha or an external path
+ if linkAvailabilityMap[entry.trimmedPath] {
+ html += fmt.Sprintf(
+ ` `,
+ img.pagePathFor(entry.trimmedPath),
+ img.binaryPathFor(entry.trimmedPath),
+ entry.sizeWAsAttr(), entry.sizeHAsAttr())
+ } else { // If is a non-existent hypha
+ html += fmt.Sprintf(`Hypha %s does not exist `, img.pagePathFor(entry.trimmedPath), entry.trimmedPath)
+ }
+ html += entry.descriptionAsHtml(img.hyphaName)
html += ` `
}
- return ``
+ return html + ` `
}
diff --git a/markup/lexer.go b/markup/lexer.go
index cad18d7..08476d6 100644
--- a/markup/lexer.go
+++ b/markup/lexer.go
@@ -12,6 +12,9 @@ var HyphaExists func(string) bool
// HyphaAccess holds function that accesses a hypha by its name.
var HyphaAccess func(string) (rawText, binaryHtml string, err error)
+// HyphaIterate is a function that iterates all hypha names existing.
+var HyphaIterate func(func(string))
+
// GemLexerState is used by markup parser to remember what is going on.
type GemLexerState struct {
// Name of hypha being parsed
@@ -21,7 +24,7 @@ type GemLexerState struct {
id int
buf string
// Temporaries
- img Img
+ img *Img
}
type Line struct {
@@ -45,13 +48,17 @@ func geminiLineToAST(line string, state *GemLexerState, ast *[]Line) {
*ast = append(*ast, Line{id: state.id, contents: text})
}
+ // Process empty lines depending on the current state
if "" == strings.TrimSpace(line) {
- if state.where == "list" {
+ switch state.where {
+ case "list":
state.where = ""
addLine(state.buf + "")
- } else if state.where == "number" {
+ case "number":
state.where = ""
addLine(state.buf + "")
+ case "pre":
+ state.buf += "\n"
}
return
}
@@ -59,6 +66,9 @@ func geminiLineToAST(line string, state *GemLexerState, ast *[]Line) {
startsWith := func(token string) bool {
return strings.HasPrefix(line, token)
}
+ addHeading := func(i int) {
+ addLine(fmt.Sprintf("%s ", i, state.id, ParagraphToHtml(state.name, line[i+1:]), i))
+ }
// Beware! Usage of goto. Some may say it is considered evil but in this case it helped to make a better-structured code.
switch state.where {
@@ -77,7 +87,7 @@ func geminiLineToAST(line string, state *GemLexerState, ast *[]Line) {
imgState:
if shouldGoBackToNormal := state.img.Process(line); shouldGoBackToNormal {
state.where = ""
- addLine(state.img)
+ addLine(*state.img)
}
return
@@ -142,23 +152,17 @@ normalState:
goto numberState
case startsWith("###### "):
- addLine(fmt.Sprintf(
- "%s ", state.id, line[7:]))
+ addHeading(6)
case startsWith("##### "):
- addLine(fmt.Sprintf(
- "%s ", state.id, line[6:]))
+ addHeading(5)
case startsWith("#### "):
- addLine(fmt.Sprintf(
- "%s ", state.id, line[5:]))
+ addHeading(4)
case startsWith("### "):
- addLine(fmt.Sprintf(
- "%s ", state.id, line[4:]))
+ addHeading(3)
case startsWith("## "):
- addLine(fmt.Sprintf(
- "%s ", state.id, line[3:]))
+ addHeading(2)
case startsWith("# "):
- addLine(fmt.Sprintf(
- "%s ", state.id, line[2:]))
+ addHeading(1)
case startsWith(">"):
addLine(fmt.Sprintf(
@@ -173,8 +177,13 @@ normalState:
case line == "----":
*ast = append(*ast, Line{id: -1, contents: " "})
case MatchesImg(line):
- state.where = "img"
- state.img = ImgFromFirstLine(line, state.name)
+ img, shouldGoBackToNormal := ImgFromFirstLine(line, state.name)
+ if shouldGoBackToNormal {
+ addLine(*img)
+ } else {
+ state.where = "img"
+ state.img = img
+ }
default:
addLine(fmt.Sprintf("%s
", state.id, ParagraphToHtml(state.name, line)))
}
diff --git a/markup/lexer_test.go b/markup/lexer_test.go
index 3298a36..b19bec2 100644
--- a/markup/lexer_test.go
+++ b/markup/lexer_test.go
@@ -41,7 +41,7 @@ func TestLex(t *testing.T) {
`},
{6, "text
"},
{7, "more text
"},
- {8, `some link
`},
+ {8, `some link
`},
{9, ``},
diff --git a/markup/paragraph.go b/markup/paragraph.go
index 1eb1f9b..8f90085 100644
--- a/markup/paragraph.go
+++ b/markup/paragraph.go
@@ -17,6 +17,7 @@ const (
spanSuper
spanSub
spanMark
+ spanStrike
spanLink
)
@@ -78,7 +79,7 @@ func getTextNode(input *bytes.Buffer) string {
escaping = false
} else if b == '\\' {
escaping = true
- } else if strings.IndexByte("/*`^,![", b) >= 0 {
+ } else if strings.IndexByte("/*`^,![~", b) >= 0 {
input.UnreadByte()
break
} else {
@@ -127,6 +128,9 @@ func ParagraphToHtml(hyphaName, input string) string {
case startsWith("!!"):
ret.WriteString(tagFromState(spanMark, tagState, "mark", "!!"))
p.Next(2)
+ case startsWith("~~"):
+ ret.WriteString(tagFromState(spanMark, tagState, "s", "~~"))
+ p.Next(2)
case startsWith("[["):
ret.WriteString(getLinkNode(p, hyphaName))
default:
@@ -149,6 +153,8 @@ func ParagraphToHtml(hyphaName, input string) string {
ret.WriteString(tagFromState(spanSub, tagState, "sub", ",,"))
case spanMark:
ret.WriteString(tagFromState(spanMark, tagState, "mark", "!!"))
+ case spanStrike:
+ ret.WriteString(tagFromState(spanMark, tagState, "s", "~~"))
case spanLink:
ret.WriteString(tagFromState(spanLink, tagState, "a", "[["))
}
diff --git a/markup/paragraph_test.go b/markup/paragraph_test.go
index 6fd1d91..71bd5fc 100644
--- a/markup/paragraph_test.go
+++ b/markup/paragraph_test.go
@@ -32,6 +32,7 @@ func TestParagraphToHtml(t *testing.T) {
{"Embedded //italic//", "Embedded italic "},
{"double //italian// //text//", "double italian text "},
{"it has `mono`", "it has mono
"},
+ {"it has ~~strike~~", "it has strike "},
{"this is a left **bold", "this is a left bold "},
{"this line has a ,comma, two of them", "this line has a ,comma, two of them"},
{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."},
diff --git a/markup/parser.go b/markup/parser.go
index dd7e110..23887d1 100644
--- a/markup/parser.go
+++ b/markup/parser.go
@@ -21,6 +21,8 @@ func Parse(ast []Line, from, to int, state GemParserState) (html string) {
html += v.ToHtml()
case string:
html += v
+ default:
+ html += "Unknown"
}
}
}
diff --git a/metarrhiza b/metarrhiza
index d78a907..7828352 160000
--- a/metarrhiza
+++ b/metarrhiza
@@ -1 +1 @@
-Subproject commit d78a90718ae9c36f7a114901ddad84b0e23221b3
+Subproject commit 7828352598c19afe5f2e13df0219656ac7b44c9c
diff --git a/mime_test.go b/mime_test.go
deleted file mode 100644
index 05b2d08..0000000
--- a/mime_test.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package main
-
-import (
- "testing"
-)
-
-func TestMimeData(t *testing.T) {
- check := func(ext string, expectedIsText bool, expectedMimeId int) {
- isText, mimeId := mimeData(ext)
- if isText != expectedIsText || mimeId != expectedMimeId {
- t.Error(ext, isText, mimeId)
- }
- }
- check(".txt", true, int(TextPlain))
- check(".gmi", true, int(TextGemini))
- check(".bin", false, int(BinaryOctet))
- check(".jpg", false, int(BinaryJpeg))
- check(".bin", false, int(BinaryOctet))
-}
diff --git a/mycocredentials.json b/mycocredentials.json
new file mode 100644
index 0000000..f2ad452
--- /dev/null
+++ b/mycocredentials.json
@@ -0,0 +1,24 @@
+[
+ {
+ "name": "admin",
+ "password": "mycorrhiza",
+ "group": "admin"
+ },
+ {
+ "name": "weird_fish",
+ "password": "DeepestOcean",
+ "group": "moderator"
+ },
+ {
+ "name": "king_of_limbs",
+ "password": "ambush",
+ "group": "trusted"
+ },
+ {
+ "name": "paranoid_android",
+ "password": "ok computer",
+ "group": "editor"
+ }
+]
+
+
diff --git a/templates/auth.qtpl b/templates/auth.qtpl
new file mode 100644
index 0000000..bbb7d25
--- /dev/null
+++ b/templates/auth.qtpl
@@ -0,0 +1,60 @@
+{% import "github.com/bouncepaw/mycorrhiza/user" %}
+
+{% func LoginHTML() %}
+
+
+ {% if user.AuthUsed %}
+ Login
+
+ {% else %}
+ Administrator of this wiki have not configured any authorization method. You can make edits anonymously.
+ ā Go home
+ {% endif %}
+
+
+{% endfunc %}
+
+{% func LoginErrorHTML(err string) %}
+
+
+ {% switch err %}
+ {% case "unknown username" %}
+ Unknown username.
+ {% case "wrong password" %}
+ Wrong password.
+ {% default %}
+ {%s err %}
+ {% endswitch %}
+ ā Try again
+
+
+{% endfunc %}
+
+{% func LogoutHTML(can bool) %}
+
+
+ {% if can %}
+ Log out?
+ Confirm
+ Cancel
+ {% else %}
+ You cannot log out because you are not logged in.
+ Login
+ ā Home
+ {% endif %}
+
+
+{% endfunc %}
diff --git a/templates/auth.qtpl.go b/templates/auth.qtpl.go
new file mode 100644
index 0000000..27d13fe
--- /dev/null
+++ b/templates/auth.qtpl.go
@@ -0,0 +1,218 @@
+// Code generated by qtc from "auth.qtpl". DO NOT EDIT.
+// See https://github.com/valyala/quicktemplate for details.
+
+//line templates/auth.qtpl:1
+package templates
+
+//line templates/auth.qtpl:1
+import "github.com/bouncepaw/mycorrhiza/user"
+
+//line templates/auth.qtpl:3
+import (
+ qtio422016 "io"
+
+ qt422016 "github.com/valyala/quicktemplate"
+)
+
+//line templates/auth.qtpl:3
+var (
+ _ = qtio422016.Copy
+ _ = qt422016.AcquireByteBuffer
+)
+
+//line templates/auth.qtpl:3
+func StreamLoginHTML(qw422016 *qt422016.Writer) {
+//line templates/auth.qtpl:3
+ qw422016.N().S(`
+
+
+ `)
+//line templates/auth.qtpl:6
+ if user.AuthUsed {
+//line templates/auth.qtpl:6
+ qw422016.N().S(`
+ Login
+
+ `)
+//line templates/auth.qtpl:22
+ } else {
+//line templates/auth.qtpl:22
+ qw422016.N().S(`
+ Administrator of this wiki have not configured any authorization method. You can make edits anonymously.
+ ā Go home
+ `)
+//line templates/auth.qtpl:25
+ }
+//line templates/auth.qtpl:25
+ qw422016.N().S(`
+
+
+`)
+//line templates/auth.qtpl:28
+}
+
+//line templates/auth.qtpl:28
+func WriteLoginHTML(qq422016 qtio422016.Writer) {
+//line templates/auth.qtpl:28
+ qw422016 := qt422016.AcquireWriter(qq422016)
+//line templates/auth.qtpl:28
+ StreamLoginHTML(qw422016)
+//line templates/auth.qtpl:28
+ qt422016.ReleaseWriter(qw422016)
+//line templates/auth.qtpl:28
+}
+
+//line templates/auth.qtpl:28
+func LoginHTML() string {
+//line templates/auth.qtpl:28
+ qb422016 := qt422016.AcquireByteBuffer()
+//line templates/auth.qtpl:28
+ WriteLoginHTML(qb422016)
+//line templates/auth.qtpl:28
+ qs422016 := string(qb422016.B)
+//line templates/auth.qtpl:28
+ qt422016.ReleaseByteBuffer(qb422016)
+//line templates/auth.qtpl:28
+ return qs422016
+//line templates/auth.qtpl:28
+}
+
+//line templates/auth.qtpl:30
+func StreamLoginErrorHTML(qw422016 *qt422016.Writer, err string) {
+//line templates/auth.qtpl:30
+ qw422016.N().S(`
+
+
+ `)
+//line templates/auth.qtpl:33
+ switch err {
+//line templates/auth.qtpl:34
+ case "unknown username":
+//line templates/auth.qtpl:34
+ qw422016.N().S(`
+ Unknown username.
+ `)
+//line templates/auth.qtpl:36
+ case "wrong password":
+//line templates/auth.qtpl:36
+ qw422016.N().S(`
+ Wrong password.
+ `)
+//line templates/auth.qtpl:38
+ default:
+//line templates/auth.qtpl:38
+ qw422016.N().S(`
+ `)
+//line templates/auth.qtpl:39
+ qw422016.E().S(err)
+//line templates/auth.qtpl:39
+ qw422016.N().S(`
+ `)
+//line templates/auth.qtpl:40
+ }
+//line templates/auth.qtpl:40
+ qw422016.N().S(`
+ ā Try again
+
+
+`)
+//line templates/auth.qtpl:44
+}
+
+//line templates/auth.qtpl:44
+func WriteLoginErrorHTML(qq422016 qtio422016.Writer, err string) {
+//line templates/auth.qtpl:44
+ qw422016 := qt422016.AcquireWriter(qq422016)
+//line templates/auth.qtpl:44
+ StreamLoginErrorHTML(qw422016, err)
+//line templates/auth.qtpl:44
+ qt422016.ReleaseWriter(qw422016)
+//line templates/auth.qtpl:44
+}
+
+//line templates/auth.qtpl:44
+func LoginErrorHTML(err string) string {
+//line templates/auth.qtpl:44
+ qb422016 := qt422016.AcquireByteBuffer()
+//line templates/auth.qtpl:44
+ WriteLoginErrorHTML(qb422016, err)
+//line templates/auth.qtpl:44
+ qs422016 := string(qb422016.B)
+//line templates/auth.qtpl:44
+ qt422016.ReleaseByteBuffer(qb422016)
+//line templates/auth.qtpl:44
+ return qs422016
+//line templates/auth.qtpl:44
+}
+
+//line templates/auth.qtpl:46
+func StreamLogoutHTML(qw422016 *qt422016.Writer, can bool) {
+//line templates/auth.qtpl:46
+ qw422016.N().S(`
+
+
+ `)
+//line templates/auth.qtpl:49
+ if can {
+//line templates/auth.qtpl:49
+ qw422016.N().S(`
+ Log out?
+ Confirm
+ Cancel
+ `)
+//line templates/auth.qtpl:53
+ } else {
+//line templates/auth.qtpl:53
+ qw422016.N().S(`
+ You cannot log out because you are not logged in.
+ Login
+ ā Home
+ `)
+//line templates/auth.qtpl:57
+ }
+//line templates/auth.qtpl:57
+ qw422016.N().S(`
+
+
+`)
+//line templates/auth.qtpl:60
+}
+
+//line templates/auth.qtpl:60
+func WriteLogoutHTML(qq422016 qtio422016.Writer, can bool) {
+//line templates/auth.qtpl:60
+ qw422016 := qt422016.AcquireWriter(qq422016)
+//line templates/auth.qtpl:60
+ StreamLogoutHTML(qw422016, can)
+//line templates/auth.qtpl:60
+ qt422016.ReleaseWriter(qw422016)
+//line templates/auth.qtpl:60
+}
+
+//line templates/auth.qtpl:60
+func LogoutHTML(can bool) string {
+//line templates/auth.qtpl:60
+ qb422016 := qt422016.AcquireByteBuffer()
+//line templates/auth.qtpl:60
+ WriteLogoutHTML(qb422016, can)
+//line templates/auth.qtpl:60
+ qs422016 := string(qb422016.B)
+//line templates/auth.qtpl:60
+ qt422016.ReleaseByteBuffer(qb422016)
+//line templates/auth.qtpl:60
+ return qs422016
+//line templates/auth.qtpl:60
+}
diff --git a/templates/common.qtpl b/templates/common.qtpl
index c199fa2..321da84 100644
--- a/templates/common.qtpl
+++ b/templates/common.qtpl
@@ -1,3 +1,7 @@
+{% import "net/http" %}
+{% import "github.com/bouncepaw/mycorrhiza/user" %}
+{% import "github.com/bouncepaw/mycorrhiza/util" %}
+
This is the seen on top of many pages.
{% code
type navEntry struct {
@@ -10,23 +14,38 @@ var navEntries = []navEntry{
{"text", "Raw text"},
{"history", "History"},
{"revision", "NOT REACHED"},
- {"delete-ask", "Delete"},
{"rename-ask", "Rename"},
+ {"delete-ask", "Delete"},
}
%}
-{% func navHTML(hyphaName, navType string, revisionHash ...string) %}
-
+{% func navHTML(rq *http.Request, hyphaName, navType string, revisionHash ...string) %}
+{% code
+ u := user.FromRequest(rq).OrAnon()
+%}
+
{%- for _, entry := range navEntries -%}
{%- if navType == "revision" && entry.path == "revision" -%}
{%s revisionHash[0] %}
{%- elseif navType == entry.path -%}
{%s entry.title %}
- {%- elseif entry.path != "revision"-%}
+ {%- elseif entry.path != "revision" && u.Group.CanAccessRoute(entry.path) -%}
{%s entry.title %}
{%- endif -%}
{%- endfor -%}
+ {%s= userMenuHTML(u) %}
{% endfunc %}
+
+{% func userMenuHTML(u *user.User) %}
+
+ {% if u.Group == user.UserAnon %}
+ Login
+ {% else %}
+ {%s u.Name %}
+ {% endif %}
+
+{% endfunc %}
+
diff --git a/templates/common.qtpl.go b/templates/common.qtpl.go
index 11baf9a..de53098 100644
--- a/templates/common.qtpl.go
+++ b/templates/common.qtpl.go
@@ -1,25 +1,34 @@
// Code generated by qtc from "common.qtpl". DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details.
-// This is the seen on top of many pages.
-
-//line templates/common.qtpl:2
+//line templates/common.qtpl:1
package templates
+//line templates/common.qtpl:1
+import "net/http"
+
//line templates/common.qtpl:2
+import "github.com/bouncepaw/mycorrhiza/user"
+
+//line templates/common.qtpl:3
+import "github.com/bouncepaw/mycorrhiza/util"
+
+// This is the seen on top of many pages.
+
+//line templates/common.qtpl:6
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
-//line templates/common.qtpl:2
+//line templates/common.qtpl:6
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
-//line templates/common.qtpl:3
+//line templates/common.qtpl:7
type navEntry struct {
path string
title string
@@ -31,87 +40,163 @@ var navEntries = []navEntry{
{"text", "Raw text"},
{"history", "History"},
{"revision", "NOT REACHED"},
- {"delete-ask", "Delete"},
{"rename-ask", "Rename"},
+ {"delete-ask", "Delete"},
}
-//line templates/common.qtpl:18
-func streamnavHTML(qw422016 *qt422016.Writer, hyphaName, navType string, revisionHash ...string) {
-//line templates/common.qtpl:18
+//line templates/common.qtpl:22
+func streamnavHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, navType string, revisionHash ...string) {
+//line templates/common.qtpl:22
qw422016.N().S(`
-
+`)
+//line templates/common.qtpl:24
+ u := user.FromRequest(rq).OrAnon()
+
+//line templates/common.qtpl:25
+ qw422016.N().S(`
+
`)
-//line templates/common.qtpl:21
+//line templates/common.qtpl:28
for _, entry := range navEntries {
-//line templates/common.qtpl:22
+//line templates/common.qtpl:29
if navType == "revision" && entry.path == "revision" {
-//line templates/common.qtpl:22
+//line templates/common.qtpl:29
qw422016.N().S(` `)
-//line templates/common.qtpl:23
+//line templates/common.qtpl:30
qw422016.E().S(revisionHash[0])
-//line templates/common.qtpl:23
+//line templates/common.qtpl:30
qw422016.N().S(`
`)
-//line templates/common.qtpl:24
+//line templates/common.qtpl:31
} else if navType == entry.path {
-//line templates/common.qtpl:24
+//line templates/common.qtpl:31
qw422016.N().S(` `)
-//line templates/common.qtpl:25
+//line templates/common.qtpl:32
qw422016.E().S(entry.title)
-//line templates/common.qtpl:25
+//line templates/common.qtpl:32
qw422016.N().S(`
`)
-//line templates/common.qtpl:26
- } else if entry.path != "revision" {
-//line templates/common.qtpl:26
+//line templates/common.qtpl:33
+ } else if entry.path != "revision" && u.Group.CanAccessRoute(entry.path) {
+//line templates/common.qtpl:33
qw422016.N().S(` `)
-//line templates/common.qtpl:27
+//line templates/common.qtpl:34
qw422016.E().S(entry.title)
-//line templates/common.qtpl:27
+//line templates/common.qtpl:34
qw422016.N().S(`
`)
-//line templates/common.qtpl:28
+//line templates/common.qtpl:35
}
-//line templates/common.qtpl:29
+//line templates/common.qtpl:36
}
-//line templates/common.qtpl:29
- qw422016.N().S(`
+//line templates/common.qtpl:36
+ qw422016.N().S(` `)
+//line templates/common.qtpl:37
+ qw422016.N().S(userMenuHTML(u))
+//line templates/common.qtpl:37
+ qw422016.N().S(`
+
`)
-//line templates/common.qtpl:32
+//line templates/common.qtpl:40
}
-//line templates/common.qtpl:32
-func writenavHTML(qq422016 qtio422016.Writer, hyphaName, navType string, revisionHash ...string) {
-//line templates/common.qtpl:32
+//line templates/common.qtpl:40
+func writenavHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, navType string, revisionHash ...string) {
+//line templates/common.qtpl:40
qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/common.qtpl:32
- streamnavHTML(qw422016, hyphaName, navType, revisionHash...)
-//line templates/common.qtpl:32
+//line templates/common.qtpl:40
+ streamnavHTML(qw422016, rq, hyphaName, navType, revisionHash...)
+//line templates/common.qtpl:40
qt422016.ReleaseWriter(qw422016)
-//line templates/common.qtpl:32
+//line templates/common.qtpl:40
}
-//line templates/common.qtpl:32
-func navHTML(hyphaName, navType string, revisionHash ...string) string {
-//line templates/common.qtpl:32
+//line templates/common.qtpl:40
+func navHTML(rq *http.Request, hyphaName, navType string, revisionHash ...string) string {
+//line templates/common.qtpl:40
qb422016 := qt422016.AcquireByteBuffer()
-//line templates/common.qtpl:32
- writenavHTML(qb422016, hyphaName, navType, revisionHash...)
-//line templates/common.qtpl:32
+//line templates/common.qtpl:40
+ writenavHTML(qb422016, rq, hyphaName, navType, revisionHash...)
+//line templates/common.qtpl:40
qs422016 := string(qb422016.B)
-//line templates/common.qtpl:32
+//line templates/common.qtpl:40
qt422016.ReleaseByteBuffer(qb422016)
-//line templates/common.qtpl:32
+//line templates/common.qtpl:40
return qs422016
-//line templates/common.qtpl:32
+//line templates/common.qtpl:40
+}
+
+//line templates/common.qtpl:42
+func streamuserMenuHTML(qw422016 *qt422016.Writer, u *user.User) {
+//line templates/common.qtpl:42
+ qw422016.N().S(`
+
+ `)
+//line templates/common.qtpl:44
+ if u.Group == user.UserAnon {
+//line templates/common.qtpl:44
+ qw422016.N().S(`
+ Login
+ `)
+//line templates/common.qtpl:46
+ } else {
+//line templates/common.qtpl:46
+ qw422016.N().S(`
+ `)
+//line templates/common.qtpl:47
+ qw422016.E().S(u.Name)
+//line templates/common.qtpl:47
+ qw422016.N().S(`
+ `)
+//line templates/common.qtpl:48
+ }
+//line templates/common.qtpl:48
+ qw422016.N().S(`
+
+`)
+//line templates/common.qtpl:50
+}
+
+//line templates/common.qtpl:50
+func writeuserMenuHTML(qq422016 qtio422016.Writer, u *user.User) {
+//line templates/common.qtpl:50
+ qw422016 := qt422016.AcquireWriter(qq422016)
+//line templates/common.qtpl:50
+ streamuserMenuHTML(qw422016, u)
+//line templates/common.qtpl:50
+ qt422016.ReleaseWriter(qw422016)
+//line templates/common.qtpl:50
+}
+
+//line templates/common.qtpl:50
+func userMenuHTML(u *user.User) string {
+//line templates/common.qtpl:50
+ qb422016 := qt422016.AcquireByteBuffer()
+//line templates/common.qtpl:50
+ writeuserMenuHTML(qb422016, u)
+//line templates/common.qtpl:50
+ qs422016 := string(qb422016.B)
+//line templates/common.qtpl:50
+ qt422016.ReleaseByteBuffer(qb422016)
+//line templates/common.qtpl:50
+ return qs422016
+//line templates/common.qtpl:50
}
diff --git a/templates/css.qtpl b/templates/css.qtpl
index 55e86d6..7a6f6e3 100644
--- a/templates/css.qtpl
+++ b/templates/css.qtpl
@@ -21,10 +21,11 @@ blockquote {border-left: 4px black solid; margin-left: 0; padding-left: 1rem;}
.wikilink_new {color:#a55858;}
.wikilink_new:visited {color:#a55858;}
.wikilink_external::after {content:"š"; margin-left: .5rem; font-size: small; text-decoration: none; align: bottom;}
-p code {background-color:#eee; padding: .1rem .3rem; border-radius: .25rem; font-size: 90%; }
-.codeblock {background-color:#eee; padding:.5rem; font-size:16px; white-space: pre-wrap;}
+article code {background-color:#eee; padding: .1rem .3rem; border-radius: .25rem; font-size: 90%; }
+article pre.codeblock {background-color:#eee; padding:.5rem; white-space: pre-wrap; border-radius: .25rem;}
+.codeblock code {padding:0; font-size:15px;}
.transclusion code, .transclusion .codeblock {background-color:#ddd;}
-.transclusion {background-color:#eee; }
+.transclusion {background-color:#eee; border-radius: .25rem; }
.transclusion__content > *:not(.binary-container) {margin: 0.5rem; }
.transclusion__link {display: block; text-align: right; font-style: italic; margin-top: 0.5rem; color: black; text-decoration: none;}
.transclusion__link::before {content: "ā ";}
@@ -33,16 +34,21 @@ p code {background-color:#eee; padding: .1rem .3rem; border-radius: .25rem; font
.binary-container_with-video video,
.binary-container_with-audio audio {width: 100%}
.navi-title a {text-decoration:none;}
-.img-gallery img { max-width: 100%; }
+.img-gallery { text-align: center; margin-top: .25rem; }
+.img-gallery_many-images { background-color: #eee; border-radius: .25rem; padding: .5rem; }
+.img-gallery img { max-width: 100%; max-height: 50vh; }
figure { margin: 0; }
+figcaption { padding-bottom: .5rem; }
nav ul {display:flex; padding-left:0; flex-wrap:wrap; margin-top:0;}
nav ul li {list-style-type:none;margin-right:1rem;}
#new-name {width:100%;}
+.navlinks__user {font-style:italic;}
.rc-entry { display: grid; list-style-type: none; padding: .25rem; background-color: #eee; grid-template-columns: 1fr 1fr; }
.rc-entry__time { font-style: italic; }
.rc-entry__hash { font-style: italic; text-align: right; }
.rc-entry__links { grid-column: 1 / span 2; }
+.rc-entry__author { font-style: italic; }
{% endfunc %}
diff --git a/templates/css.qtpl.go b/templates/css.qtpl.go
index 09519d7..72a25fc 100644
--- a/templates/css.qtpl.go
+++ b/templates/css.qtpl.go
@@ -43,10 +43,11 @@ blockquote {border-left: 4px black solid; margin-left: 0; padding-left: 1rem;}
.wikilink_new {color:#a55858;}
.wikilink_new:visited {color:#a55858;}
.wikilink_external::after {content:"š"; margin-left: .5rem; font-size: small; text-decoration: none; align: bottom;}
-p code {background-color:#eee; padding: .1rem .3rem; border-radius: .25rem; font-size: 90%; }
-.codeblock {background-color:#eee; padding:.5rem; font-size:16px; white-space: pre-wrap;}
+article code {background-color:#eee; padding: .1rem .3rem; border-radius: .25rem; font-size: 90%; }
+article pre.codeblock {background-color:#eee; padding:.5rem; white-space: pre-wrap; border-radius: .25rem;}
+.codeblock code {padding:0; font-size:15px;}
.transclusion code, .transclusion .codeblock {background-color:#ddd;}
-.transclusion {background-color:#eee; }
+.transclusion {background-color:#eee; border-radius: .25rem; }
.transclusion__content > *:not(.binary-container) {margin: 0.5rem; }
.transclusion__link {display: block; text-align: right; font-style: italic; margin-top: 0.5rem; color: black; text-decoration: none;}
.transclusion__link::before {content: "ā ";}
@@ -55,44 +56,49 @@ p code {background-color:#eee; padding: .1rem .3rem; border-radius: .25rem; font
.binary-container_with-video video,
.binary-container_with-audio audio {width: 100%}
.navi-title a {text-decoration:none;}
-.img-gallery img { max-width: 100%; }
+.img-gallery { text-align: center; margin-top: .25rem; }
+.img-gallery_many-images { background-color: #eee; border-radius: .25rem; padding: .5rem; }
+.img-gallery img { max-width: 100%; max-height: 50vh; }
figure { margin: 0; }
+figcaption { padding-bottom: .5rem; }
nav ul {display:flex; padding-left:0; flex-wrap:wrap; margin-top:0;}
nav ul li {list-style-type:none;margin-right:1rem;}
#new-name {width:100%;}
+.navlinks__user {font-style:italic;}
.rc-entry { display: grid; list-style-type: none; padding: .25rem; background-color: #eee; grid-template-columns: 1fr 1fr; }
.rc-entry__time { font-style: italic; }
.rc-entry__hash { font-style: italic; text-align: right; }
.rc-entry__links { grid-column: 1 / span 2; }
+.rc-entry__author { font-style: italic; }
`)
-//line templates/css.qtpl:48
+//line templates/css.qtpl:54
}
-//line templates/css.qtpl:48
+//line templates/css.qtpl:54
func WriteDefaultCSS(qq422016 qtio422016.Writer) {
-//line templates/css.qtpl:48
+//line templates/css.qtpl:54
qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/css.qtpl:48
+//line templates/css.qtpl:54
StreamDefaultCSS(qw422016)
-//line templates/css.qtpl:48
+//line templates/css.qtpl:54
qt422016.ReleaseWriter(qw422016)
-//line templates/css.qtpl:48
+//line templates/css.qtpl:54
}
-//line templates/css.qtpl:48
+//line templates/css.qtpl:54
func DefaultCSS() string {
-//line templates/css.qtpl:48
+//line templates/css.qtpl:54
qb422016 := qt422016.AcquireByteBuffer()
-//line templates/css.qtpl:48
+//line templates/css.qtpl:54
WriteDefaultCSS(qb422016)
-//line templates/css.qtpl:48
+//line templates/css.qtpl:54
qs422016 := string(qb422016.B)
-//line templates/css.qtpl:48
+//line templates/css.qtpl:54
qt422016.ReleaseByteBuffer(qb422016)
-//line templates/css.qtpl:48
+//line templates/css.qtpl:54
return qs422016
-//line templates/css.qtpl:48
+//line templates/css.qtpl:54
}
diff --git a/templates/delete.qtpl b/templates/delete.qtpl
index 285c605..d91c851 100644
--- a/templates/delete.qtpl
+++ b/templates/delete.qtpl
@@ -1,7 +1,9 @@
+{% import "net/http" %}
+
This dialog is to be shown to a user when they try to delete a hypha.
-{% func DeleteAskHTML(hyphaName string, isOld bool) %}
+{% func DeleteAskHTML(rq *http.Request, hyphaName string, isOld bool) %}
-{%= navHTML(hyphaName, "delete-ask") %}
+{%= navHTML(rq, hyphaName, "delete-ask") %}
{% if isOld %}
Delete {%s hyphaName %}?
diff --git a/templates/delete.qtpl.go b/templates/delete.qtpl.go
index f016775..4f58b04 100644
--- a/templates/delete.qtpl.go
+++ b/templates/delete.qtpl.go
@@ -1,151 +1,154 @@
// Code generated by qtc from "delete.qtpl". DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details.
-// This dialog is to be shown to a user when they try to delete a hypha.
-
-//line templates/delete.qtpl:2
+//line templates/delete.qtpl:1
package templates
-//line templates/delete.qtpl:2
+//line templates/delete.qtpl:1
+import "net/http"
+
+// This dialog is to be shown to a user when they try to delete a hypha.
+
+//line templates/delete.qtpl:4
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
-//line templates/delete.qtpl:2
+//line templates/delete.qtpl:4
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
-//line templates/delete.qtpl:2
-func StreamDeleteAskHTML(qw422016 *qt422016.Writer, hyphaName string, isOld bool) {
-//line templates/delete.qtpl:2
+//line templates/delete.qtpl:4
+func StreamDeleteAskHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName string, isOld bool) {
+//line templates/delete.qtpl:4
qw422016.N().S(`
`)
-//line templates/delete.qtpl:4
- streamnavHTML(qw422016, hyphaName, "delete-ask")
-//line templates/delete.qtpl:4
+//line templates/delete.qtpl:6
+ streamnavHTML(qw422016, rq, hyphaName, "delete-ask")
+//line templates/delete.qtpl:6
qw422016.N().S(`
`)
-//line templates/delete.qtpl:5
+//line templates/delete.qtpl:7
if isOld {
-//line templates/delete.qtpl:5
+//line templates/delete.qtpl:7
qw422016.N().S(`
Delete `)
-//line templates/delete.qtpl:7
+//line templates/delete.qtpl:9
qw422016.E().S(hyphaName)
-//line templates/delete.qtpl:7
+//line templates/delete.qtpl:9
qw422016.N().S(`?
Do you really want to delete hypha `)
-//line templates/delete.qtpl:8
+//line templates/delete.qtpl:10
qw422016.E().S(hyphaName)
-//line templates/delete.qtpl:8
+//line templates/delete.qtpl:10
qw422016.N().S(` ?
In this version of MycorrhizaWiki you cannot undelete a deleted hypha but the history can still be accessed.
Confirm
Cancel
`)
-//line templates/delete.qtpl:13
+//line templates/delete.qtpl:15
} else {
-//line templates/delete.qtpl:13
+//line templates/delete.qtpl:15
qw422016.N().S(`
`)
-//line templates/delete.qtpl:14
+//line templates/delete.qtpl:16
streamcannotDeleteDueToNonExistence(qw422016, hyphaName)
-//line templates/delete.qtpl:14
+//line templates/delete.qtpl:16
qw422016.N().S(`
`)
-//line templates/delete.qtpl:15
+//line templates/delete.qtpl:17
}
-//line templates/delete.qtpl:15
+//line templates/delete.qtpl:17
qw422016.N().S(`
`)
-//line templates/delete.qtpl:17
+//line templates/delete.qtpl:19
}
-//line templates/delete.qtpl:17
-func WriteDeleteAskHTML(qq422016 qtio422016.Writer, hyphaName string, isOld bool) {
-//line templates/delete.qtpl:17
+//line templates/delete.qtpl:19
+func WriteDeleteAskHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName string, isOld bool) {
+//line templates/delete.qtpl:19
qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/delete.qtpl:17
- StreamDeleteAskHTML(qw422016, hyphaName, isOld)
-//line templates/delete.qtpl:17
+//line templates/delete.qtpl:19
+ StreamDeleteAskHTML(qw422016, rq, hyphaName, isOld)
+//line templates/delete.qtpl:19
qt422016.ReleaseWriter(qw422016)
-//line templates/delete.qtpl:17
+//line templates/delete.qtpl:19
}
-//line templates/delete.qtpl:17
-func DeleteAskHTML(hyphaName string, isOld bool) string {
-//line templates/delete.qtpl:17
+//line templates/delete.qtpl:19
+func DeleteAskHTML(rq *http.Request, hyphaName string, isOld bool) string {
+//line templates/delete.qtpl:19
qb422016 := qt422016.AcquireByteBuffer()
-//line templates/delete.qtpl:17
- WriteDeleteAskHTML(qb422016, hyphaName, isOld)
-//line templates/delete.qtpl:17
+//line templates/delete.qtpl:19
+ WriteDeleteAskHTML(qb422016, rq, hyphaName, isOld)
+//line templates/delete.qtpl:19
qs422016 := string(qb422016.B)
-//line templates/delete.qtpl:17
+//line templates/delete.qtpl:19
qt422016.ReleaseByteBuffer(qb422016)
-//line templates/delete.qtpl:17
+//line templates/delete.qtpl:19
return qs422016
-//line templates/delete.qtpl:17
+//line templates/delete.qtpl:19
}
-//line templates/delete.qtpl:19
+//line templates/delete.qtpl:21
func streamcannotDeleteDueToNonExistence(qw422016 *qt422016.Writer, hyphaName string) {
-//line templates/delete.qtpl:19
+//line templates/delete.qtpl:21
qw422016.N().S(`
Cannot delete `)
-//line templates/delete.qtpl:21
+//line templates/delete.qtpl:23
qw422016.E().S(hyphaName)
-//line templates/delete.qtpl:21
+//line templates/delete.qtpl:23
qw422016.N().S(`
This hypha does not exist.
Go back
`)
-//line templates/delete.qtpl:25
+//line templates/delete.qtpl:27
}
-//line templates/delete.qtpl:25
+//line templates/delete.qtpl:27
func writecannotDeleteDueToNonExistence(qq422016 qtio422016.Writer, hyphaName string) {
-//line templates/delete.qtpl:25
+//line templates/delete.qtpl:27
qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/delete.qtpl:25
+//line templates/delete.qtpl:27
streamcannotDeleteDueToNonExistence(qw422016, hyphaName)
-//line templates/delete.qtpl:25
+//line templates/delete.qtpl:27
qt422016.ReleaseWriter(qw422016)
-//line templates/delete.qtpl:25
+//line templates/delete.qtpl:27
}
-//line templates/delete.qtpl:25
+//line templates/delete.qtpl:27
func cannotDeleteDueToNonExistence(hyphaName string) string {
-//line templates/delete.qtpl:25
+//line templates/delete.qtpl:27
qb422016 := qt422016.AcquireByteBuffer()
-//line templates/delete.qtpl:25
+//line templates/delete.qtpl:27
writecannotDeleteDueToNonExistence(qb422016, hyphaName)
-//line templates/delete.qtpl:25
+//line templates/delete.qtpl:27
qs422016 := string(qb422016.B)
-//line templates/delete.qtpl:25
+//line templates/delete.qtpl:27
qt422016.ReleaseByteBuffer(qb422016)
-//line templates/delete.qtpl:25
+//line templates/delete.qtpl:27
return qs422016
-//line templates/delete.qtpl:25
+//line templates/delete.qtpl:27
}
diff --git a/templates/http_mutators.qtpl b/templates/http_mutators.qtpl
index b7f9a0f..049f8ae 100644
--- a/templates/http_mutators.qtpl
+++ b/templates/http_mutators.qtpl
@@ -1,13 +1,16 @@
-{% func EditHTML(hyphaName, textAreaFill, warning string) %}
-
- Edit {%s hyphaName %}
- {%s= warning %}
-
-
+{% import "net/http" %}
+
+{% func EditHTML(rq *http.Request, hyphaName, textAreaFill, warning string) %}
+
+{%s= navHTML(rq, hyphaName, "edit") %}
+ Edit {%s hyphaName %}
+ {%s= warning %}
+
+ {%s textAreaFill %}
+
+
+ Cancel
+
+
{% endfunc %}
diff --git a/templates/http_mutators.qtpl.go b/templates/http_mutators.qtpl.go
index c57d482..62a24a1 100644
--- a/templates/http_mutators.qtpl.go
+++ b/templates/http_mutators.qtpl.go
@@ -5,79 +5,87 @@
package templates
//line templates/http_mutators.qtpl:1
+import "net/http"
+
+//line templates/http_mutators.qtpl:3
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
-//line templates/http_mutators.qtpl:1
+//line templates/http_mutators.qtpl:3
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
-//line templates/http_mutators.qtpl:1
-func StreamEditHTML(qw422016 *qt422016.Writer, hyphaName, textAreaFill, warning string) {
-//line templates/http_mutators.qtpl:1
- qw422016.N().S(`
-
- Edit `)
//line templates/http_mutators.qtpl:3
- qw422016.E().S(hyphaName)
+func StreamEditHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, textAreaFill, warning string) {
//line templates/http_mutators.qtpl:3
- qw422016.N().S(`
- `)
-//line templates/http_mutators.qtpl:4
- qw422016.N().S(warning)
-//line templates/http_mutators.qtpl:4
qw422016.N().S(`
-
- `)
-//line templates/http_mutators.qtpl:7
- qw422016.E().S(textAreaFill)
-//line templates/http_mutators.qtpl:7
- qw422016.N().S(`
-
-
- Cancel
-
-
+
`)
+//line templates/http_mutators.qtpl:5
+ qw422016.N().S(navHTML(rq, hyphaName, "edit"))
+//line templates/http_mutators.qtpl:5
+ qw422016.N().S(`
+ Edit `)
+//line templates/http_mutators.qtpl:6
+ qw422016.E().S(hyphaName)
+//line templates/http_mutators.qtpl:6
+ qw422016.N().S(`
+ `)
+//line templates/http_mutators.qtpl:7
+ qw422016.N().S(warning)
+//line templates/http_mutators.qtpl:7
+ qw422016.N().S(`
+
+ `)
+//line templates/http_mutators.qtpl:10
+ qw422016.E().S(textAreaFill)
+//line templates/http_mutators.qtpl:10
+ qw422016.N().S(`
+
+
+ Cancel
+
+
+`)
+//line templates/http_mutators.qtpl:16
}
-//line templates/http_mutators.qtpl:13
-func WriteEditHTML(qq422016 qtio422016.Writer, hyphaName, textAreaFill, warning string) {
-//line templates/http_mutators.qtpl:13
+//line templates/http_mutators.qtpl:16
+func WriteEditHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, textAreaFill, warning string) {
+//line templates/http_mutators.qtpl:16
qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/http_mutators.qtpl:13
- StreamEditHTML(qw422016, hyphaName, textAreaFill, warning)
-//line templates/http_mutators.qtpl:13
+//line templates/http_mutators.qtpl:16
+ StreamEditHTML(qw422016, rq, hyphaName, textAreaFill, warning)
+//line templates/http_mutators.qtpl:16
qt422016.ReleaseWriter(qw422016)
-//line templates/http_mutators.qtpl:13
+//line templates/http_mutators.qtpl:16
}
-//line templates/http_mutators.qtpl:13
-func EditHTML(hyphaName, textAreaFill, warning string) string {
-//line templates/http_mutators.qtpl:13
+//line templates/http_mutators.qtpl:16
+func EditHTML(rq *http.Request, hyphaName, textAreaFill, warning string) string {
+//line templates/http_mutators.qtpl:16
qb422016 := qt422016.AcquireByteBuffer()
-//line templates/http_mutators.qtpl:13
- WriteEditHTML(qb422016, hyphaName, textAreaFill, warning)
-//line templates/http_mutators.qtpl:13
+//line templates/http_mutators.qtpl:16
+ WriteEditHTML(qb422016, rq, hyphaName, textAreaFill, warning)
+//line templates/http_mutators.qtpl:16
qs422016 := string(qb422016.B)
-//line templates/http_mutators.qtpl:13
+//line templates/http_mutators.qtpl:16
qt422016.ReleaseByteBuffer(qb422016)
-//line templates/http_mutators.qtpl:13
+//line templates/http_mutators.qtpl:16
return qs422016
-//line templates/http_mutators.qtpl:13
+//line templates/http_mutators.qtpl:16
}
diff --git a/templates/http_readers.qtpl b/templates/http_readers.qtpl
index c138602..a1cb2f3 100644
--- a/templates/http_readers.qtpl
+++ b/templates/http_readers.qtpl
@@ -1,6 +1,9 @@
-{% func HistoryHTML(hyphaName, tbody string) %}
+{% import "net/http" %}
+{% import "github.com/bouncepaw/mycorrhiza/user" %}
+
+{% func HistoryHTML(rq *http.Request, hyphaName, tbody string) %}
-{%= navHTML(hyphaName, "history") %}
+{%= navHTML(rq, hyphaName, "history") %}
@@ -16,9 +19,9 @@
{% endfunc %}
-{% func RevisionHTML(hyphaName, naviTitle, contents, tree, revHash string) %}
+{% func RevisionHTML(rq *http.Request, hyphaName, naviTitle, contents, tree, revHash string) %}
-{%= navHTML(hyphaName, "revision", revHash) %}
+{%= navHTML(rq, hyphaName, "revision", revHash) %}
Please note that viewing binary parts of hyphae is not supported in history for now.
{%s= naviTitle %}
@@ -32,9 +35,9 @@
{% endfunc %}
If `contents` == "", a helpful message is shown instead.
-{% func PageHTML(hyphaName, naviTitle, contents, tree string) %}
+{% func PageHTML(rq *http.Request, hyphaName, naviTitle, contents, tree string) %}
-{%= navHTML(hyphaName, "page") %}
+{%= navHTML(rq, hyphaName, "page") %}
{%s= naviTitle %}
{% if contents == "" %}
@@ -44,14 +47,16 @@ If `contents` == "", a helpful message is shown instead.
{% endif %}
+{% if u := user.FromRequest(rq).OrAnon(); !user.AuthUsed || u.Group > user.UserAnon %}
- Upload new binary part
+ Upload a new attachment
+{% endif %}
diff --git a/templates/http_readers.qtpl.go b/templates/http_readers.qtpl.go
index 2bea84c..3ffac47 100644
--- a/templates/http_readers.qtpl.go
+++ b/templates/http_readers.qtpl.go
@@ -5,27 +5,33 @@
package templates
//line templates/http_readers.qtpl:1
+import "net/http"
+
+//line templates/http_readers.qtpl:2
+import "github.com/bouncepaw/mycorrhiza/user"
+
+//line templates/http_readers.qtpl:4
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
-//line templates/http_readers.qtpl:1
+//line templates/http_readers.qtpl:4
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
-//line templates/http_readers.qtpl:1
-func StreamHistoryHTML(qw422016 *qt422016.Writer, hyphaName, tbody string) {
-//line templates/http_readers.qtpl:1
+//line templates/http_readers.qtpl:4
+func StreamHistoryHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, tbody string) {
+//line templates/http_readers.qtpl:4
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:3
- streamnavHTML(qw422016, hyphaName, "history")
-//line templates/http_readers.qtpl:3
+//line templates/http_readers.qtpl:6
+ streamnavHTML(qw422016, rq, hyphaName, "history")
+//line templates/http_readers.qtpl:6
qw422016.N().S(`
@@ -37,196 +43,206 @@ func StreamHistoryHTML(qw422016 *qt422016.Writer, hyphaName, tbody string) {
`)
-//line templates/http_readers.qtpl:13
+//line templates/http_readers.qtpl:16
qw422016.N().S(tbody)
-//line templates/http_readers.qtpl:13
+//line templates/http_readers.qtpl:16
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:17
+//line templates/http_readers.qtpl:20
}
-//line templates/http_readers.qtpl:17
-func WriteHistoryHTML(qq422016 qtio422016.Writer, hyphaName, tbody string) {
-//line templates/http_readers.qtpl:17
+//line templates/http_readers.qtpl:20
+func WriteHistoryHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, tbody string) {
+//line templates/http_readers.qtpl:20
qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/http_readers.qtpl:17
- StreamHistoryHTML(qw422016, hyphaName, tbody)
-//line templates/http_readers.qtpl:17
+//line templates/http_readers.qtpl:20
+ StreamHistoryHTML(qw422016, rq, hyphaName, tbody)
+//line templates/http_readers.qtpl:20
qt422016.ReleaseWriter(qw422016)
-//line templates/http_readers.qtpl:17
+//line templates/http_readers.qtpl:20
}
-//line templates/http_readers.qtpl:17
-func HistoryHTML(hyphaName, tbody string) string {
-//line templates/http_readers.qtpl:17
+//line templates/http_readers.qtpl:20
+func HistoryHTML(rq *http.Request, hyphaName, tbody string) string {
+//line templates/http_readers.qtpl:20
qb422016 := qt422016.AcquireByteBuffer()
-//line templates/http_readers.qtpl:17
- WriteHistoryHTML(qb422016, hyphaName, tbody)
-//line templates/http_readers.qtpl:17
+//line templates/http_readers.qtpl:20
+ WriteHistoryHTML(qb422016, rq, hyphaName, tbody)
+//line templates/http_readers.qtpl:20
qs422016 := string(qb422016.B)
-//line templates/http_readers.qtpl:17
+//line templates/http_readers.qtpl:20
qt422016.ReleaseByteBuffer(qb422016)
-//line templates/http_readers.qtpl:17
+//line templates/http_readers.qtpl:20
return qs422016
-//line templates/http_readers.qtpl:17
+//line templates/http_readers.qtpl:20
}
-//line templates/http_readers.qtpl:19
-func StreamRevisionHTML(qw422016 *qt422016.Writer, hyphaName, naviTitle, contents, tree, revHash string) {
-//line templates/http_readers.qtpl:19
+//line templates/http_readers.qtpl:22
+func StreamRevisionHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, naviTitle, contents, tree, revHash string) {
+//line templates/http_readers.qtpl:22
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:21
- streamnavHTML(qw422016, hyphaName, "revision", revHash)
-//line templates/http_readers.qtpl:21
+//line templates/http_readers.qtpl:24
+ streamnavHTML(qw422016, rq, hyphaName, "revision", revHash)
+//line templates/http_readers.qtpl:24
qw422016.N().S(`
Please note that viewing binary parts of hyphae is not supported in history for now.
`)
-//line templates/http_readers.qtpl:24
+//line templates/http_readers.qtpl:27
qw422016.N().S(naviTitle)
-//line templates/http_readers.qtpl:24
+//line templates/http_readers.qtpl:27
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:25
+//line templates/http_readers.qtpl:28
qw422016.N().S(contents)
-//line templates/http_readers.qtpl:25
+//line templates/http_readers.qtpl:28
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:29
+//line templates/http_readers.qtpl:32
qw422016.N().S(tree)
-//line templates/http_readers.qtpl:29
+//line templates/http_readers.qtpl:32
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:32
+//line templates/http_readers.qtpl:35
}
-//line templates/http_readers.qtpl:32
-func WriteRevisionHTML(qq422016 qtio422016.Writer, hyphaName, naviTitle, contents, tree, revHash string) {
-//line templates/http_readers.qtpl:32
+//line templates/http_readers.qtpl:35
+func WriteRevisionHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, naviTitle, contents, tree, revHash string) {
+//line templates/http_readers.qtpl:35
qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/http_readers.qtpl:32
- StreamRevisionHTML(qw422016, hyphaName, naviTitle, contents, tree, revHash)
-//line templates/http_readers.qtpl:32
+//line templates/http_readers.qtpl:35
+ StreamRevisionHTML(qw422016, rq, hyphaName, naviTitle, contents, tree, revHash)
+//line templates/http_readers.qtpl:35
qt422016.ReleaseWriter(qw422016)
-//line templates/http_readers.qtpl:32
+//line templates/http_readers.qtpl:35
}
-//line templates/http_readers.qtpl:32
-func RevisionHTML(hyphaName, naviTitle, contents, tree, revHash string) string {
-//line templates/http_readers.qtpl:32
+//line templates/http_readers.qtpl:35
+func RevisionHTML(rq *http.Request, hyphaName, naviTitle, contents, tree, revHash string) string {
+//line templates/http_readers.qtpl:35
qb422016 := qt422016.AcquireByteBuffer()
-//line templates/http_readers.qtpl:32
- WriteRevisionHTML(qb422016, hyphaName, naviTitle, contents, tree, revHash)
-//line templates/http_readers.qtpl:32
+//line templates/http_readers.qtpl:35
+ WriteRevisionHTML(qb422016, rq, hyphaName, naviTitle, contents, tree, revHash)
+//line templates/http_readers.qtpl:35
qs422016 := string(qb422016.B)
-//line templates/http_readers.qtpl:32
+//line templates/http_readers.qtpl:35
qt422016.ReleaseByteBuffer(qb422016)
-//line templates/http_readers.qtpl:32
+//line templates/http_readers.qtpl:35
return qs422016
-//line templates/http_readers.qtpl:32
+//line templates/http_readers.qtpl:35
}
// If `contents` == "", a helpful message is shown instead.
-//line templates/http_readers.qtpl:35
-func StreamPageHTML(qw422016 *qt422016.Writer, hyphaName, naviTitle, contents, tree string) {
-//line templates/http_readers.qtpl:35
+//line templates/http_readers.qtpl:38
+func StreamPageHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, naviTitle, contents, tree string) {
+//line templates/http_readers.qtpl:38
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:37
- streamnavHTML(qw422016, hyphaName, "page")
-//line templates/http_readers.qtpl:37
+//line templates/http_readers.qtpl:40
+ streamnavHTML(qw422016, rq, hyphaName, "page")
+//line templates/http_readers.qtpl:40
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:39
+//line templates/http_readers.qtpl:42
qw422016.N().S(naviTitle)
-//line templates/http_readers.qtpl:39
+//line templates/http_readers.qtpl:42
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:40
+//line templates/http_readers.qtpl:43
if contents == "" {
-//line templates/http_readers.qtpl:40
+//line templates/http_readers.qtpl:43
qw422016.N().S(`
This hypha has no text. Why not create it ?
`)
-//line templates/http_readers.qtpl:42
+//line templates/http_readers.qtpl:45
} else {
-//line templates/http_readers.qtpl:42
+//line templates/http_readers.qtpl:45
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:43
+//line templates/http_readers.qtpl:46
qw422016.N().S(contents)
-//line templates/http_readers.qtpl:43
+//line templates/http_readers.qtpl:46
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:44
+//line templates/http_readers.qtpl:47
}
-//line templates/http_readers.qtpl:44
+//line templates/http_readers.qtpl:47
qw422016.N().S(`
+`)
+//line templates/http_readers.qtpl:50
+ if u := user.FromRequest(rq).OrAnon(); !user.AuthUsed || u.Group > user.UserAnon {
+//line templates/http_readers.qtpl:50
+ qw422016.N().S(`
- Upload new binary part
+ Upload a new attachment
+`)
+//line templates/http_readers.qtpl:59
+ }
+//line templates/http_readers.qtpl:59
+ qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:56
+//line templates/http_readers.qtpl:61
qw422016.N().S(tree)
-//line templates/http_readers.qtpl:56
+//line templates/http_readers.qtpl:61
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:59
+//line templates/http_readers.qtpl:64
}
-//line templates/http_readers.qtpl:59
-func WritePageHTML(qq422016 qtio422016.Writer, hyphaName, naviTitle, contents, tree string) {
-//line templates/http_readers.qtpl:59
+//line templates/http_readers.qtpl:64
+func WritePageHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, naviTitle, contents, tree string) {
+//line templates/http_readers.qtpl:64
qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/http_readers.qtpl:59
- StreamPageHTML(qw422016, hyphaName, naviTitle, contents, tree)
-//line templates/http_readers.qtpl:59
+//line templates/http_readers.qtpl:64
+ StreamPageHTML(qw422016, rq, hyphaName, naviTitle, contents, tree)
+//line templates/http_readers.qtpl:64
qt422016.ReleaseWriter(qw422016)
-//line templates/http_readers.qtpl:59
+//line templates/http_readers.qtpl:64
}
-//line templates/http_readers.qtpl:59
-func PageHTML(hyphaName, naviTitle, contents, tree string) string {
-//line templates/http_readers.qtpl:59
+//line templates/http_readers.qtpl:64
+func PageHTML(rq *http.Request, hyphaName, naviTitle, contents, tree string) string {
+//line templates/http_readers.qtpl:64
qb422016 := qt422016.AcquireByteBuffer()
-//line templates/http_readers.qtpl:59
- WritePageHTML(qb422016, hyphaName, naviTitle, contents, tree)
-//line templates/http_readers.qtpl:59
+//line templates/http_readers.qtpl:64
+ WritePageHTML(qb422016, rq, hyphaName, naviTitle, contents, tree)
+//line templates/http_readers.qtpl:64
qs422016 := string(qb422016.B)
-//line templates/http_readers.qtpl:59
+//line templates/http_readers.qtpl:64
qt422016.ReleaseByteBuffer(qb422016)
-//line templates/http_readers.qtpl:59
+//line templates/http_readers.qtpl:64
return qs422016
-//line templates/http_readers.qtpl:59
+//line templates/http_readers.qtpl:64
}
diff --git a/templates/rename.qtpl b/templates/rename.qtpl
index 77e4e56..b68d912 100644
--- a/templates/rename.qtpl
+++ b/templates/rename.qtpl
@@ -1,7 +1,8 @@
+{% import "net/http" %}
This dialog is to be shown to a user when they try to rename a hypha.
-{% func RenameAskHTML(hyphaName string, isOld bool) %}
+{% func RenameAskHTML(rq *http.Request, hyphaName string, isOld bool) %}
-{%= navHTML(hyphaName, "rename-ask") %}
+{%= navHTML(rq, hyphaName, "rename-ask") %}
{%- if isOld -%}
Rename {%s hyphaName %}
diff --git a/templates/rename.qtpl.go b/templates/rename.qtpl.go
index 9ae1c45..34f4ed9 100644
--- a/templates/rename.qtpl.go
+++ b/templates/rename.qtpl.go
@@ -1,55 +1,58 @@
// Code generated by qtc from "rename.qtpl". DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details.
-// This dialog is to be shown to a user when they try to rename a hypha.
-
-//line templates/rename.qtpl:2
+//line templates/rename.qtpl:1
package templates
-//line templates/rename.qtpl:2
+//line templates/rename.qtpl:1
+import "net/http"
+
+// This dialog is to be shown to a user when they try to rename a hypha.
+
+//line templates/rename.qtpl:3
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
-//line templates/rename.qtpl:2
+//line templates/rename.qtpl:3
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
-//line templates/rename.qtpl:2
-func StreamRenameAskHTML(qw422016 *qt422016.Writer, hyphaName string, isOld bool) {
-//line templates/rename.qtpl:2
+//line templates/rename.qtpl:3
+func StreamRenameAskHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName string, isOld bool) {
+//line templates/rename.qtpl:3
qw422016.N().S(`
`)
-//line templates/rename.qtpl:4
- streamnavHTML(qw422016, hyphaName, "rename-ask")
-//line templates/rename.qtpl:4
+//line templates/rename.qtpl:5
+ streamnavHTML(qw422016, rq, hyphaName, "rename-ask")
+//line templates/rename.qtpl:5
qw422016.N().S(`
`)
-//line templates/rename.qtpl:5
+//line templates/rename.qtpl:6
if isOld {
-//line templates/rename.qtpl:5
+//line templates/rename.qtpl:6
qw422016.N().S(`
`)
-//line templates/rename.qtpl:24
+//line templates/rename.qtpl:25
} else {
-//line templates/rename.qtpl:24
+//line templates/rename.qtpl:25
qw422016.N().S(` `)
-//line templates/rename.qtpl:25
+//line templates/rename.qtpl:26
streamcannotRenameDueToNonExistence(qw422016, hyphaName)
-//line templates/rename.qtpl:25
+//line templates/rename.qtpl:26
qw422016.N().S(`
`)
-//line templates/rename.qtpl:26
+//line templates/rename.qtpl:27
}
-//line templates/rename.qtpl:26
+//line templates/rename.qtpl:27
qw422016.N().S(`
`)
-//line templates/rename.qtpl:28
+//line templates/rename.qtpl:29
}
-//line templates/rename.qtpl:28
-func WriteRenameAskHTML(qq422016 qtio422016.Writer, hyphaName string, isOld bool) {
-//line templates/rename.qtpl:28
+//line templates/rename.qtpl:29
+func WriteRenameAskHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName string, isOld bool) {
+//line templates/rename.qtpl:29
qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/rename.qtpl:28
- StreamRenameAskHTML(qw422016, hyphaName, isOld)
-//line templates/rename.qtpl:28
+//line templates/rename.qtpl:29
+ StreamRenameAskHTML(qw422016, rq, hyphaName, isOld)
+//line templates/rename.qtpl:29
qt422016.ReleaseWriter(qw422016)
-//line templates/rename.qtpl:28
+//line templates/rename.qtpl:29
}
-//line templates/rename.qtpl:28
-func RenameAskHTML(hyphaName string, isOld bool) string {
-//line templates/rename.qtpl:28
+//line templates/rename.qtpl:29
+func RenameAskHTML(rq *http.Request, hyphaName string, isOld bool) string {
+//line templates/rename.qtpl:29
qb422016 := qt422016.AcquireByteBuffer()
-//line templates/rename.qtpl:28
- WriteRenameAskHTML(qb422016, hyphaName, isOld)
-//line templates/rename.qtpl:28
+//line templates/rename.qtpl:29
+ WriteRenameAskHTML(qb422016, rq, hyphaName, isOld)
+//line templates/rename.qtpl:29
qs422016 := string(qb422016.B)
-//line templates/rename.qtpl:28
+//line templates/rename.qtpl:29
qt422016.ReleaseByteBuffer(qb422016)
-//line templates/rename.qtpl:28
+//line templates/rename.qtpl:29
return qs422016
-//line templates/rename.qtpl:28
+//line templates/rename.qtpl:29
}
-//line templates/rename.qtpl:30
+//line templates/rename.qtpl:31
func streamcannotRenameDueToNonExistence(qw422016 *qt422016.Writer, hyphaName string) {
-//line templates/rename.qtpl:30
+//line templates/rename.qtpl:31
qw422016.N().S(`
Cannot rename `)
-//line templates/rename.qtpl:32
+//line templates/rename.qtpl:33
qw422016.E().S(hyphaName)
-//line templates/rename.qtpl:32
+//line templates/rename.qtpl:33
qw422016.N().S(`
This hypha does not exist.
Go back
`)
-//line templates/rename.qtpl:36
+//line templates/rename.qtpl:37
}
-//line templates/rename.qtpl:36
+//line templates/rename.qtpl:37
func writecannotRenameDueToNonExistence(qq422016 qtio422016.Writer, hyphaName string) {
-//line templates/rename.qtpl:36
+//line templates/rename.qtpl:37
qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/rename.qtpl:36
+//line templates/rename.qtpl:37
streamcannotRenameDueToNonExistence(qw422016, hyphaName)
-//line templates/rename.qtpl:36
+//line templates/rename.qtpl:37
qt422016.ReleaseWriter(qw422016)
-//line templates/rename.qtpl:36
+//line templates/rename.qtpl:37
}
-//line templates/rename.qtpl:36
+//line templates/rename.qtpl:37
func cannotRenameDueToNonExistence(hyphaName string) string {
-//line templates/rename.qtpl:36
+//line templates/rename.qtpl:37
qb422016 := qt422016.AcquireByteBuffer()
-//line templates/rename.qtpl:36
+//line templates/rename.qtpl:37
writecannotRenameDueToNonExistence(qb422016, hyphaName)
-//line templates/rename.qtpl:36
+//line templates/rename.qtpl:37
qs422016 := string(qb422016.B)
-//line templates/rename.qtpl:36
+//line templates/rename.qtpl:37
qt422016.ReleaseByteBuffer(qb422016)
-//line templates/rename.qtpl:36
+//line templates/rename.qtpl:37
return qs422016
-//line templates/rename.qtpl:36
+//line templates/rename.qtpl:37
}
diff --git a/user/fs.go b/user/fs.go
new file mode 100644
index 0000000..f47c423
--- /dev/null
+++ b/user/fs.go
@@ -0,0 +1,69 @@
+package user
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "log"
+ "os"
+
+ "github.com/adrg/xdg"
+ "github.com/bouncepaw/mycorrhiza/util"
+)
+
+func PopulateFixedUserStorage() {
+ contents, err := ioutil.ReadFile(util.FixedCredentialsPath)
+ if err != nil {
+ log.Fatal(err)
+ }
+ err = json.Unmarshal(contents, &UserStorage.Users)
+ if err != nil {
+ log.Fatal(err)
+ }
+ for _, user := range UserStorage.Users {
+ user.Group = groupFromString(user.GroupString)
+ }
+ log.Println("Found", len(UserStorage.Users), "fixed users")
+
+ contents, err = ioutil.ReadFile(tokenStoragePath())
+ if os.IsNotExist(err) {
+ return
+ }
+ if err != nil {
+ log.Fatal(err)
+ }
+ var tmp map[string]string
+ err = json.Unmarshal(contents, &tmp)
+ if err != nil {
+ log.Fatal(err)
+ }
+ for token, username := range tmp {
+ user := UserStorage.userByName(username)
+ UserStorage.Tokens[token] = user
+ }
+ log.Println("Found", len(tmp), "active sessions")
+}
+
+func dumpTokens() {
+ tmp := make(map[string]string)
+ for token, user := range UserStorage.Tokens {
+ tmp[token] = user.Name
+ }
+ blob, err := json.Marshal(tmp)
+ if err != nil {
+ log.Println(err)
+ } else {
+ ioutil.WriteFile(tokenStoragePath(), blob, 0644)
+ }
+}
+
+// Return path to tokens.json.
+func tokenStoragePath() string {
+ dir, err := xdg.DataFile("mycorrhiza/tokens.json")
+ if err != nil {
+ // Yes, it is unix-only, but function above rarely fails, so this block is probably never reached.
+ path := "/home/" + os.Getenv("HOME") + "/.local/share/mycorrhiza/tokens.json"
+ os.MkdirAll(path, 0777)
+ return path
+ }
+ return dir
+}
diff --git a/user/group.go b/user/group.go
new file mode 100644
index 0000000..6a9a296
--- /dev/null
+++ b/user/group.go
@@ -0,0 +1,70 @@
+package user
+
+import (
+ "log"
+ "net/http"
+)
+
+func groupFromString(s string) UserGroup {
+ switch s {
+ case "admin":
+ return UserAdmin
+ case "moderator":
+ return UserModerator
+ case "trusted":
+ return UserTrusted
+ case "editor":
+ return UserEditor
+ default:
+ log.Fatal("Unknown user group", s)
+ return UserAnon
+ }
+}
+
+// UserGroup represents a group that a user is part of.
+type UserGroup int
+
+const (
+ // UserAnon is the default user group which all unauthorized visitors have.
+ UserAnon UserGroup = iota
+ // UserEditor is a user who can edit and upload stuff.
+ UserEditor
+ // UserTrusted is a trusted editor who can also rename stuff.
+ UserTrusted
+ // UserModerator is a moderator who can also delete stuff.
+ UserModerator
+ // UserAdmin can do everything.
+ UserAdmin
+)
+
+var minimalRights = map[string]UserGroup{
+ "edit": UserEditor,
+ "upload-binary": UserEditor,
+ "upload-text": UserEditor,
+ "rename-ask": UserTrusted,
+ "rename-confirm": UserTrusted,
+ "delete-ask": UserModerator,
+ "delete-confirm": UserModerator,
+ "reindex": UserAdmin,
+}
+
+func (ug UserGroup) CanAccessRoute(route string) bool {
+ if !AuthUsed {
+ return true
+ }
+ if minimalRight, ok := minimalRights[route]; ok {
+ if ug >= minimalRight {
+ return true
+ }
+ return false
+ }
+ return true
+}
+
+func CanProceed(rq *http.Request, route string) bool {
+ return FromRequest(rq).OrAnon().CanProceed(route)
+}
+
+func (u *User) CanProceed(route string) bool {
+ return u.Group.CanAccessRoute(route)
+}
diff --git a/user/user.go b/user/user.go
new file mode 100644
index 0000000..7169760
--- /dev/null
+++ b/user/user.go
@@ -0,0 +1,138 @@
+package user
+
+import (
+ "log"
+ "net/http"
+ "time"
+
+ "github.com/bouncepaw/mycorrhiza/util"
+)
+
+func (u *User) OrAnon() *User {
+ if u == nil {
+ return &User{}
+ }
+ return u
+}
+
+func LogoutFromRequest(w http.ResponseWriter, rq *http.Request) {
+ cookieFromUser, err := rq.Cookie("mycorrhiza_token")
+ if err == nil {
+ http.SetCookie(w, cookie("token", "", time.Unix(0, 0)))
+ terminateSession(cookieFromUser.Value)
+ }
+}
+
+func (us *FixedUserStorage) userByToken(token string) *User {
+ if user, ok := us.Tokens[token]; ok {
+ return user
+ }
+ return nil
+}
+
+func (us *FixedUserStorage) userByName(username string) *User {
+ for _, user := range us.Users {
+ if user.Name == username {
+ return user
+ }
+ }
+ return nil
+}
+
+func FromRequest(rq *http.Request) *User {
+ cookie, err := rq.Cookie("mycorrhiza_token")
+ if err != nil {
+ return nil
+ }
+ return UserStorage.userByToken(cookie.Value).OrAnon()
+}
+
+func LoginDataHTTP(w http.ResponseWriter, rq *http.Request, username, password string) string {
+ w.Header().Set("Content-Type", "text/html;charset=utf-8")
+ if !HasUsername(username) {
+ w.WriteHeader(http.StatusBadRequest)
+ log.Println("Unknown username", username, "was entered")
+ return "unknown username"
+ }
+ if !CredentialsOK(username, password) {
+ w.WriteHeader(http.StatusBadRequest)
+ log.Println("A wrong password was entered for username", username)
+ return "wrong password"
+ }
+ token, err := AddSession(username)
+ if err != nil {
+ log.Println(err)
+ w.WriteHeader(http.StatusBadRequest)
+ return err.Error()
+ }
+ http.SetCookie(w, cookie("token", token, time.Now().Add(14*24*time.Hour)))
+ return ""
+}
+
+// AddSession saves a session for `username` and returns a token to use.
+func AddSession(username string) (string, error) {
+ token, err := util.RandomString(16)
+ if err == nil {
+ for _, user := range UserStorage.Users {
+ if user.Name == username {
+ UserStorage.Tokens[token] = user
+ go dumpTokens()
+ }
+ }
+ log.Println("New token for", username, "is", token)
+ }
+ return token, err
+}
+
+func terminateSession(token string) {
+ delete(UserStorage.Tokens, token)
+ go dumpTokens()
+}
+
+func HasUsername(username string) bool {
+ for _, user := range UserStorage.Users {
+ if user.Name == username {
+ return true
+ }
+ }
+ return false
+}
+
+func CredentialsOK(username, password string) bool {
+ for _, user := range UserStorage.Users {
+ if user.Name == username && user.Password == password {
+ return true
+ }
+ }
+ return false
+}
+
+type FixedUserStorage struct {
+ Users []*User
+ Tokens map[string]*User
+}
+
+var UserStorage = FixedUserStorage{Tokens: make(map[string]*User)}
+
+// AuthUsed shows if a method of authentication is used. You should set it by yourself.
+var AuthUsed bool
+
+// User is a user.
+type User struct {
+ // Name is a username. It must follow hypha naming rules.
+ Name string `json:"name"`
+ // Group the user is part of.
+ Group UserGroup `json:"-"`
+ GroupString string `json:"group"`
+ Password string `json:"password"`
+}
+
+// A handy cookie constructor
+func cookie(name_suffix, val string, t time.Time) *http.Cookie {
+ return &http.Cookie{
+ Name: "mycorrhiza_" + name_suffix,
+ Value: val,
+ Expires: t,
+ Path: "/",
+ }
+}
diff --git a/util/util.go b/util/util.go
index 8458d4f..745bf96 100644
--- a/util/util.go
+++ b/util/util.go
@@ -1,15 +1,20 @@
package util
import (
+ "crypto/rand"
+ "encoding/hex"
"net/http"
"strings"
)
var (
- ServerPort string
- HomePage string
- SiteTitle string
- WikiDir string
+ ServerPort string
+ HomePage string
+ SiteTitle string
+ WikiDir string
+ UserTree string
+ AuthMethod string
+ FixedCredentialsPath string
)
// ShorterPath is used by handlerList to display shorter path to the files. It simply strips WikiDir.
@@ -41,3 +46,11 @@ func FindSubhyphae(hyphaName string, hyphaIterator func(func(string))) []string
})
return subhyphae
}
+
+func RandomString(n int) (string, error) {
+ bytes := make([]byte, n)
+ if _, err := rand.Read(bytes); err != nil {
+ return "", err
+ }
+ return hex.EncodeToString(bytes), nil
+}