mirror of https://github.com/schollz/cowyo.git
Compare commits
142 Commits
Author | SHA1 | Date |
---|---|---|
Zack Scholl | ed996d9e09 | |
Zack | a58f4ca346 | |
Zack Scholl | a318863eca | |
Zack Scholl | d8d3200491 | |
Zack Scholl | f414ccc2f3 | |
Zack Scholl | 0a9b069152 | |
Zack Scholl | 4954276644 | |
Zack Scholl | 156722d23e | |
Zack Scholl | 9accd685c0 | |
Zack Scholl | 126b13fcea | |
Zack Scholl | 5a0587c3ce | |
Zack Scholl | e5a1a88a02 | |
Zack Scholl | d43cc80b53 | |
Zack Scholl | ff84acd9d7 | |
Zack | 1e46c40cc6 | |
Daniel Heath | 9e928f6b74 | |
Daniel Heath | 3b91b699e3 | |
Daniel Heath | f567f86bab | |
Daniel Heath | 0e93250cc9 | |
Daniel Heath | 82d5ac908d | |
Daniel Heath | 7a61b16e7a | |
Daniel Heath | f86dc1095a | |
Daniel Heath | a976007642 | |
Zack | 180a28cda6 | |
Daniel Heath | 08bbce1298 | |
Daniel Heath | e53123d8fa | |
Daniel Heath | d48d1458a5 | |
Daniel Heath | d9b8bfc95d | |
Daniel Heath | bc1a9ee86b | |
Daniel Heath | f4f5042245 | |
Daniel Heath | d095915f83 | |
Daniel Heath | c2cd54a12d | |
Daniel Heath | 2e80633cd4 | |
Daniel Heath | 9cf0d0e129 | |
Daniel Heath | ede4d1fba3 | |
Daniel Heath | ff5c100cf8 | |
Daniel Heath | 465e9c8e93 | |
Daniel Heath | 0d984da5b2 | |
Daniel Heath | 8944170646 | |
Zack Scholl | f21c89f7bd | |
Zack Scholl | 91178f4f29 | |
Zack Scholl | 8bc1123f1d | |
Zack Scholl | 5d0523117b | |
Zack Scholl | 6e1d64b40e | |
Zack | f9b8713404 | |
Daniel Heath | fc3030339f | |
Daniel Heath | df406ec71b | |
Daniel Heath | 2a43ebdb53 | |
Daniel Heath | d9e622fa9d | |
Daniel Heath | 446ba00fe9 | |
Daniel Heath | f729aece51 | |
Daniel Heath | e5dfacb4bb | |
Zack | 2373e339d0 | |
Tomas Peterka | c64e54a991 | |
Zack | f350d0bbad | |
Zack | 308244cfa4 | |
Zack | 50624e75e9 | |
Daniel Heath | efd4cfb0d1 | |
Zack Scholl | 4ebf9e02ef | |
Zack Scholl | c1a78c2006 | |
Zack Scholl | 8a4ba13506 | |
Zack | c78dec28eb | |
Zack | 0f8e572d43 | |
Zack Scholl | 1720711029 | |
Zack Scholl | 3c53386e08 | |
Zack | 9d4fcae7b2 | |
Daniel Heath | db48f1291b | |
Daniel Heath | 131a54a682 | |
Daniel Heath | bb475bd924 | |
Zack | dfd9aea863 | |
Daniel Heath | 9982fb5175 | |
Daniel Heath | f5f0bdb3bb | |
Daniel Heath | d65eccbe9d | |
Daniel Heath | 1a3e891dfd | |
Daniel Heath | 917d32f8a5 | |
Daniel Heath | c207077877 | |
Daniel Heath | 5e4a317b10 | |
Daniel Heath | da6d6e5d43 | |
Daniel Heath | d0bc74ec55 | |
Daniel Heath | b42750073c | |
Daniel Heath | e1934b9797 | |
Daniel Heath | 0badf719e0 | |
Zack | a0dcc9652f | |
Zack | fb6405ba1b | |
Daniel Heath | b18f40e336 | |
Daniel Heath | fa66472128 | |
Zack | 526688c7e3 | |
Zack | 31153063f2 | |
Daniel Heath | b4638476cc | |
Zack | ff420fb81d | |
Daniel Heath | dec21a80c2 | |
Zack | f998015f0c | |
Daniel Heath | fea5ef4647 | |
Daniel Heath | ecf1d2ab10 | |
Daniel Heath | 765c5788b3 | |
Daniel Heath | 76ec1c1acb | |
Daniel Heath | 89d58f5a22 | |
Daniel Heath | 2f1c0e3cd2 | |
Daniel Heath | 10d45b0c76 | |
Daniel Heath | 038a895772 | |
Daniel Heath | ff2920965d | |
Daniel Heath | a36f8e318e | |
Daniel Heath | c802fa06be | |
Daniel Heath | 81f6b2d263 | |
Daniel Heath | 5803cbdc3f | |
Daniel Heath | 5a82e77738 | |
Zack Scholl | 55368b1c1a | |
Zack Scholl | 7cc1eccb83 | |
Zack Scholl | 45436a3762 | |
Zack Scholl | 684ff4e692 | |
Zack Scholl | 63bdbb5824 | |
Zack Scholl | 85877602e0 | |
Zack Scholl | 6eacf90e18 | |
Zack Scholl | 4a916984e6 | |
Zack Scholl | 840fbf73ec | |
Zack Scholl | a2288cf129 | |
Zack Scholl | 8ccd40c105 | |
Zack Scholl | 1ef24a0347 | |
Zack | 8b3e7b0605 | |
Zack Scholl | de03d2b547 | |
Zack Scholl | d481145c5f | |
Zack Scholl | 2040dbaaa5 | |
Zack Scholl | 5ae5c91945 | |
Zack | 0ef75a919c | |
Zack | da30d15eb1 | |
Zack | 9d22d5d41d | |
Zack Scholl | c35aeca859 | |
Zack Scholl | e52d0097a9 | |
Zack Scholl | b2a01d0a6d | |
Zack Scholl | 026dd7c647 | |
Zack Scholl | 107a0dc32b | |
Zack | 683bcdea1c | |
Zack | e33ddcfddb | |
Zack Scholl | 6baa87daa4 | |
Zack | 912bf83c59 | |
Benjamin Misell | ecba3aa0ea | |
Yamil Urbina | b77c32d646 | |
Zack Scholl | bef20f3366 | |
Zack | 21047443d6 | |
Tamás Gulácsi | b83e3fd52e | |
Tamás Gulácsi | 8474b79cf1 | |
Zack Scholl | b3b5f31575 |
|
@ -0,0 +1,3 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: schollz
|
|
@ -22,3 +22,8 @@ _testmain.go
|
|||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
data/*
|
||||
cowyo
|
||||
dist
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
FROM golang:1.12-alpine as builder
|
||||
RUN apk add --no-cache git make
|
||||
RUN go get -v github.com/jteeuwen/go-bindata/go-bindata
|
||||
WORKDIR /go/cowyo
|
||||
COPY . .
|
||||
RUN make build
|
||||
|
||||
FROM alpine:latest
|
||||
VOLUME /data
|
||||
EXPOSE 8050
|
||||
COPY --from=builder /go/cowyo/cowyo /cowyo
|
||||
ENTRYPOINT ["/cowyo"]
|
||||
CMD ["--data","/data","--allow-file-uploads","--max-upload-mb","10","--host","0.0.0.0"]
|
41
Makefile
41
Makefile
|
@ -2,34 +2,53 @@
|
|||
# make -j4 release
|
||||
|
||||
VERSION=$(shell git describe)
|
||||
LDFLAGS=-ldflags "-s -w -X main.version=${VERSION}"
|
||||
LDFLAGS=-ldflags "-X main.version=${VERSION}"
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
go-bindata static/... templates/...
|
||||
build: server/bindata.go
|
||||
go build ${LDFLAGS}
|
||||
|
||||
STATICFILES := $(wildcard static/*)
|
||||
TEMPLATES := $(wildcard templates/*)
|
||||
server/bindata.go: $(STATICFILES) $(TEMPLATES)
|
||||
go-bindata -pkg server -tags '!debug' -o server/bindata.go static/... templates/...
|
||||
go fmt
|
||||
|
||||
server/bindata-debug.go: $(STATICFILES) $(TEMPLATES)
|
||||
go-bindata -pkg server -tags 'debug' -o server/bindata-debug.go -debug static/... templates/...
|
||||
go fmt
|
||||
|
||||
.PHONY: devel
|
||||
devel: server/bindata-debug.go
|
||||
go build -tags debug
|
||||
|
||||
.PHONY: quick
|
||||
quick: server/bindata.go
|
||||
go build
|
||||
|
||||
.PHONY: linuxarm
|
||||
linuxarm:
|
||||
linuxarm: server/bindata.go
|
||||
env GOOS=linux GOARCH=arm go build ${LDFLAGS} -o dist/cowyo_linux_arm
|
||||
#cd dist && upx --brute cowyo_linux_arm
|
||||
|
||||
.PHONY: linux32
|
||||
linux32: server/bindata.go
|
||||
env GOOS=linux GOARCH=386 go build ${LDFLAGS} -o dist/cowyo_linux_32bit
|
||||
#cd dist && upx --brute cowyo_linux_32bit
|
||||
|
||||
.PHONY: linux64
|
||||
linux64:
|
||||
linux64: server/bindata.go
|
||||
env GOOS=linux GOARCH=amd64 go build ${LDFLAGS} -o dist/cowyo_linux_amd64
|
||||
#cd dist && upx --brute cowyo_linux_amd64
|
||||
|
||||
.PHONY: windows
|
||||
windows:
|
||||
windows: server/bindata.go
|
||||
env GOOS=windows GOARCH=amd64 go build ${LDFLAGS} -o dist/cowyo_windows_amd64.exe
|
||||
#cd dist && upx --brute cowyo_windows_amd64.exe
|
||||
|
||||
.PHONY: osx
|
||||
osx:
|
||||
osx: server/bindata.go
|
||||
env GOOS=darwin GOARCH=amd64 go build ${LDFLAGS} -o dist/cowyo_osx_amd64
|
||||
#cd dist && upx --brute cowyo_osx_amd64
|
||||
|
||||
.PHONY: release
|
||||
release: osx windows linux64 linuxarm
|
||||
|
||||
|
||||
release: osx windows linux64 linux32 linuxarm
|
||||
|
|
45
README.md
45
README.md
|
@ -4,9 +4,12 @@
|
|||
src="/static/img/logo.png"
|
||||
width="260" height="80" border="0" alt="linkcrawler">
|
||||
<br>
|
||||
<a href="https://travis-ci.org/schollz/cowyo"><img src="https://img.shields.io/travis/schollz/cowyo.svg?style=flat-square" alt="Build Status"></a>
|
||||
<a href="https://github.com/schollz/cowyo/releases/latest"><img src="https://img.shields.io/badge/version-2.5.1-brightgreen.svg?style=flat-square" alt="Version"></a>
|
||||
</p>
|
||||
<a href="https://travis-ci.org/schollz/cowyo"><img
|
||||
src="https://img.shields.io/travis/schollz/cowyo.svg?style=flat-square"
|
||||
alt="Build Status"></a> <a
|
||||
href="https://github.com/schollz/cowyo/releases/latest"><img
|
||||
src="https://img.shields.io/badge/version-2.11.0-brightgreen.svg?style=flat-square"
|
||||
alt="Version"></a> </p>
|
||||
|
||||
<p align="center">A feature-rich wiki for minimalists</a></p>
|
||||
|
||||
|
@ -37,7 +40,7 @@ cowyo
|
|||
|
||||
and it will start a server listening on `0.0.0.0:8050`. To view it, just go to http://localhost:8050 (the server prints out the local IP for your info if you want to do LAN networking). You can change the port with `-port X`, and you can listen *only* on localhost using `-host localhost`.
|
||||
|
||||
### Running with TLS
|
||||
**Running with TLS**
|
||||
|
||||
Specify a matching pair of SSL Certificate and Key to run cowyo using https. *cowyo* will now run in a secure session.
|
||||
|
||||
|
@ -47,10 +50,38 @@ Specify a matching pair of SSL Certificate and Key to run cowyo using https. *co
|
|||
cowyo --cert "/path/to/server.crt" --key "/p/t/server.key"
|
||||
```
|
||||
|
||||
**Running with Docker**
|
||||
|
||||
You can easily get started with Docker. First pull the latest image and create the volume with:
|
||||
|
||||
```
|
||||
docker run -d -v /directory/to/data:/data -p 8050:8050 schollz/cowyo
|
||||
```
|
||||
|
||||
Then you can stop it with `docker stop cowyo` and start it again with `docker start cowyo`.
|
||||
|
||||
## Server customization
|
||||
|
||||
There are a couple of command-line flags that you can use to make *cowyo* your own micro-CMS.
|
||||
|
||||
```
|
||||
cowyo -lock 123 -default-page index.html -css mystyle.css -diary
|
||||
```
|
||||
|
||||
The `-lock` flag will automatically lock every page with the passphrase "123". Also, the default behavior will be to redirect `/` to `/index.html`. Also, every page that is published will automatically redirect to `/mypage/read` which will show the custom CSS file if it is supplied with `-css`. The `-diary` flag allows you to generate a time-stamped page instead of a random named page when you select "New".
|
||||
|
||||
## Usage
|
||||
|
||||
*cowyo* is straightforward to use. Here are some of the basic features:
|
||||
|
||||
### Publishing
|
||||
|
||||
If you hover the the top left button (the name of the page) you will see the option "Publish". Publishing will add the page to the `sitemap.xml` for crawlers to find. It will also default that page to go to the `/read` route so it can be easily viewed as a single page.
|
||||
|
||||
### View all the pages
|
||||
|
||||
To view the current list of all the pages goto to `/ls`.
|
||||
|
||||
### Editing
|
||||
|
||||
When you open a document you'll be directed to an alliterative animal (which is supposed to be easy to remember). You can write in Markdown. Saving is performed as soon as you stop writing. You can easily link pages using [[PageName]] as you edit.
|
||||
|
@ -87,6 +118,7 @@ Just like in mission impossible.
|
|||
|
||||
![Self-destructing](http://i.imgur.com/upMxFQh.gif)
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
You can run the tests using
|
||||
|
@ -98,6 +130,11 @@ $ go test ./...
|
|||
|
||||
Any contributions are welcome.
|
||||
|
||||
## Thanks
|
||||
|
||||
...to [DanielHeath](https://github.com/DanielHeath) who has introduced some stellar improvements into cowyo including supporting category pages, hot-template reloading, preventing out-of-order updates, added password access, fade-out on deleting list items, and image upload support!
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/schollz/cowyo/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
store := sessions.NewStore([]byte("secret"))
|
||||
|
||||
first := server.Site{
|
||||
PathToData: "site1",
|
||||
Debounce: 500,
|
||||
SessionStore: store,
|
||||
AllowInsecure: true,
|
||||
Fileuploads: true,
|
||||
MaxUploadSize: 2,
|
||||
}.Router()
|
||||
|
||||
second := server.Site{
|
||||
PathToData: "site2",
|
||||
Debounce: 500,
|
||||
SessionStore: store,
|
||||
AllowInsecure: true,
|
||||
Fileuploads: true,
|
||||
MaxUploadSize: 2,
|
||||
}.Router()
|
||||
panic(http.ListenAndServe("localhost:8000", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.Host, "first") {
|
||||
first.ServeHTTP(rw, r)
|
||||
} else if strings.HasPrefix(r.Host, "second") {
|
||||
second.ServeHTTP(rw, r)
|
||||
} else {
|
||||
http.NotFound(rw, r)
|
||||
}
|
||||
})))
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/schollz/cowyo/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c, err := config.ParseFile("multisite_sample.toml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
panic(c.ListenAndServe())
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
func ParseFile(path string) (Config, error) {
|
||||
c := Config{}
|
||||
if _, err := toml.DecodeFile("multisite_sample.toml", &c); err != nil {
|
||||
// handle error
|
||||
return c, err
|
||||
}
|
||||
c.SetDefaults()
|
||||
c.Validate()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Default SiteConfig
|
||||
Sites []SiteConfig
|
||||
}
|
||||
|
||||
type SiteConfig struct {
|
||||
Host *string
|
||||
Port *int
|
||||
DataDir *string
|
||||
DefaultPage *string
|
||||
AllowInsecureMarkup *bool
|
||||
Lock *string
|
||||
DebounceSave *int
|
||||
Diary *bool
|
||||
AccessCode *string
|
||||
FileUploadsAllowed *bool
|
||||
MaxFileUploadMb *uint
|
||||
MaxDocumentLength *uint
|
||||
TLS *TLSConfig
|
||||
CookieKeys []CookieKey
|
||||
}
|
||||
|
||||
type TLSConfig struct {
|
||||
CertPath string
|
||||
KeyPath string
|
||||
Port int
|
||||
}
|
||||
|
||||
type CookieKey struct {
|
||||
AuthenticateBase64 string
|
||||
EncryptBase64 string
|
||||
}
|
||||
|
||||
func (c Config) Validate() {
|
||||
for _, v := range c.Sites {
|
||||
v.sessionStore()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"crypto/rand"
|
||||
)
|
||||
var DefaultSiteConfig SiteConfig
|
||||
|
||||
|
||||
func makeAuthKey() string {
|
||||
secret := make([]byte, 32)
|
||||
_, err := rand.Read(secret)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(secret)
|
||||
}
|
||||
|
||||
func init() {
|
||||
host := "*"
|
||||
port := 8050
|
||||
debounce := 500
|
||||
dataDir := "data"
|
||||
empty := ""
|
||||
zer := uint(0)
|
||||
lots := uint(100000000)
|
||||
fal := false
|
||||
|
||||
ck := CookieKey{
|
||||
AuthenticateBase64: "",
|
||||
EncryptBase64: "",
|
||||
}
|
||||
|
||||
DefaultSiteConfig = SiteConfig{
|
||||
Host:&host,
|
||||
Port:&port,
|
||||
DataDir:&dataDir,
|
||||
DebounceSave:&debounce,
|
||||
CookieKeys: []CookieKey{ck},
|
||||
DefaultPage:&empty,
|
||||
AllowInsecureMarkup:&fal,
|
||||
Lock:&empty,
|
||||
Diary:&fal,
|
||||
AccessCode:&empty,
|
||||
FileUploadsAllowed:&fal,
|
||||
MaxFileUploadMb:&zer,
|
||||
MaxDocumentLength:&lots,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func copyDefaults(base, defaults *SiteConfig) {
|
||||
if base.Host == nil {
|
||||
base.Host = defaults.Host
|
||||
}
|
||||
if base.Port == nil {
|
||||
base.Port = defaults.Port
|
||||
}
|
||||
if base.DataDir == nil {
|
||||
base.DataDir = defaults.DataDir
|
||||
}
|
||||
if base.DefaultPage == nil {
|
||||
base.DefaultPage = defaults.DefaultPage
|
||||
}
|
||||
if base.AllowInsecureMarkup == nil {
|
||||
base.AllowInsecureMarkup = defaults.AllowInsecureMarkup
|
||||
}
|
||||
if base.Lock == nil {
|
||||
base.Lock = defaults.Lock
|
||||
}
|
||||
if base.DebounceSave == nil {
|
||||
base.DebounceSave = defaults.DebounceSave
|
||||
}
|
||||
if base.Diary == nil {
|
||||
base.Diary = defaults.Diary
|
||||
}
|
||||
if base.AccessCode == nil {
|
||||
base.AccessCode = defaults.AccessCode
|
||||
}
|
||||
if base.FileUploadsAllowed == nil {
|
||||
base.FileUploadsAllowed = defaults.FileUploadsAllowed
|
||||
}
|
||||
if base.MaxFileUploadMb == nil {
|
||||
base.MaxFileUploadMb = defaults.MaxFileUploadMb
|
||||
}
|
||||
if base.MaxDocumentLength == nil {
|
||||
base.MaxDocumentLength = defaults.MaxDocumentLength
|
||||
}
|
||||
if base.TLS == nil {
|
||||
base.TLS = defaults.TLS
|
||||
}
|
||||
if base.CookieKeys == nil {
|
||||
base.CookieKeys = defaults.CookieKeys
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) SetDefaults() {
|
||||
copyDefaults(&c.Default, &DefaultSiteConfig)
|
||||
for i := range c.Sites {
|
||||
copyDefaults(&c.Sites[i], &c.Default)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/jcelliott/lumber"
|
||||
"github.com/schollz/cowyo/server"
|
||||
"strings"
|
||||
)
|
||||
func (c Config) ListenAndServe() error {
|
||||
insecurePorts := map[int]bool{}
|
||||
securePorts := map[int]bool{}
|
||||
err := make(chan error)
|
||||
for _, s := range c.Sites {
|
||||
if !insecurePorts[*s.Port] {
|
||||
insecurePorts[*s.Port] = true
|
||||
go func(s SiteConfig) {
|
||||
err <- http.ListenAndServe(fmt.Sprintf("localhost:%d", *s.Port), c)
|
||||
}(s)
|
||||
}
|
||||
if s.TLS != nil && !securePorts[s.TLS.Port] {
|
||||
securePorts[s.TLS.Port] = true
|
||||
go func(s SiteConfig) {
|
||||
err <- http.ListenAndServeTLS(
|
||||
fmt.Sprintf("localhost:%d", s.TLS.Port),
|
||||
s.TLS.CertPath,
|
||||
s.TLS.KeyPath,
|
||||
c,
|
||||
)
|
||||
}(s)
|
||||
}
|
||||
}
|
||||
for {
|
||||
return <- err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Config) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
for i := range c.Sites {
|
||||
if c.Sites[i].MatchesRequest(r) {
|
||||
c.Sites[i].Handle(rw, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.NotFound(rw, r)
|
||||
}
|
||||
|
||||
func (s SiteConfig) MatchesRequest(r *http.Request) bool {
|
||||
sh := *s.Host
|
||||
if strings.HasPrefix(sh, "*") {
|
||||
return strings.HasSuffix(r.Host, sh[1:])
|
||||
}
|
||||
return sh == r.Host
|
||||
}
|
||||
|
||||
func (s SiteConfig) sessionStore() sessions.Store {
|
||||
keys := [][]byte{}
|
||||
for _, k := range s.CookieKeys {
|
||||
key, err := base64.StdEncoding.DecodeString(k.AuthenticateBase64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(key) != 32 {
|
||||
log.Panicf("AuthenticateBase64 key %s must be 32 bytes; suggest %s", k.AuthenticateBase64, makeAuthKey())
|
||||
}
|
||||
|
||||
keys = append(keys, key)
|
||||
key, err = base64.StdEncoding.DecodeString(k.EncryptBase64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if len(key) != 32 {
|
||||
log.Panicf("EncryptBase64 key %s must be 32 bytes, suggest %s", k.EncryptBase64, makeAuthKey())
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return sessions.NewStore(keys...)
|
||||
}
|
||||
|
||||
func (s SiteConfig) Handle(rw http.ResponseWriter, r *http.Request) {
|
||||
dataDir := strings.Replace(*s.DataDir, "${HOST}", r.Host, -1)
|
||||
|
||||
router := server.Site{
|
||||
PathToData: dataDir,
|
||||
Css: []byte{},
|
||||
DefaultPage: *s.DefaultPage,
|
||||
DefaultPassword: *s.Lock,
|
||||
Debounce: *s.DebounceSave,
|
||||
Diary: *s.Diary,
|
||||
SessionStore: s.sessionStore(),
|
||||
SecretCode: *s.AccessCode,
|
||||
AllowInsecure: *s.AllowInsecureMarkup,
|
||||
Fileuploads: *s.MaxFileUploadMb > 0,
|
||||
MaxUploadSize: *s.MaxFileUploadMb,
|
||||
Logger: lumber.NewConsoleLogger(server.LogLevel),
|
||||
MaxDocumentSize: *s.MaxDocumentLength,
|
||||
}.Router()
|
||||
|
||||
router.ServeHTTP(rw, r)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
version: "2"
|
||||
|
||||
services:
|
||||
cowyo:
|
||||
build: .
|
||||
ports:
|
||||
- 8050:8050
|
||||
volumes:
|
||||
- ./data:/data
|
|
@ -0,0 +1,35 @@
|
|||
module github.com/schollz/cowyo
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2
|
||||
github.com/bytedance/sonic v1.9.2 // indirect
|
||||
github.com/danielheath/gin-teeny-security v0.0.0-20180331042316-bb11804dd0e2
|
||||
github.com/gin-contrib/multitemplate v0.0.0-20230212012517-45920c92c271
|
||||
github.com/gin-contrib/sessions v0.0.5
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-playground/validator/v10 v10.14.1 // indirect
|
||||
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.24
|
||||
github.com/russross/blackfriday v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0
|
||||
github.com/schollz/cryptopasta v0.0.0-20170217152710-dcd61c7d42a1
|
||||
github.com/schollz/versionedtext v0.0.0-20180523061923-d8ce0957c254
|
||||
github.com/sergi/go-diff v1.3.1 // indirect
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20210228213109-c3a9aa474629
|
||||
github.com/shurcooL/go v0.0.0-20190704215121-7189cc372560 // indirect
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 // indirect
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480 // indirect
|
||||
github.com/shurcooL/highlight_go v0.0.0-20191220051317-782971ddf21b // indirect
|
||||
github.com/shurcooL/octicon v0.0.0-20230705024016-66bff059edb8 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect
|
||||
golang.org/x/arch v0.4.0 // indirect
|
||||
golang.org/x/crypto v0.11.0
|
||||
golang.org/x/net v0.12.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/urfave/cli.v1 v1.20.0
|
||||
)
|
|
@ -0,0 +1,405 @@
|
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||
github.com/bos-hieu/mongostore v0.0.2/go.mod h1:8AbbVmDEb0yqJsBrWxZIAZOxIfv/tsP8CDtdHduZHGg=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM=
|
||||
github.com/bytedance/sonic v1.9.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/danielheath/gin-teeny-security v0.0.0-20180331042316-bb11804dd0e2 h1:OU8xbewlvG+K/mPYGshgkSDJZugXeV4cvlI8r02a58o=
|
||||
github.com/danielheath/gin-teeny-security v0.0.0-20180331042316-bb11804dd0e2/go.mod h1:iufTPweOVe3TKbMOYF0OyJ5iM4pdK/D9F9dIQoQC4IE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/multitemplate v0.0.0-20230212012517-45920c92c271 h1:s+boMV47gwTyff2PL+k6V33edJpp+K5y3QPzZlRhno8=
|
||||
github.com/gin-contrib/multitemplate v0.0.0-20230212012517-45920c92c271/go.mod h1:XLLtIXoP9+9zGcEDc7gAGV3AksGPO+vzv4kXHMJSdU0=
|
||||
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
|
||||
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
|
||||
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
|
||||
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
|
||||
github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
|
||||
github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 h1:EFT6MH3igZK/dIVqgGbTqWVvkZ7wJ5iGN03SVtvvdd8=
|
||||
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25/go.mod h1:sWkGw/wsaHtRsT9zGQ/WyJCotGWG/Anow/9hsAcBWRw=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
||||
github.com/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw=
|
||||
github.com/microcosm-cc/bluemonday v1.0.24/go.mod h1:ArQySAMps0790cHSkdPEJ7bGkF2VePWH773hsJNSHf8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
|
||||
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/schollz/cryptopasta v0.0.0-20170217152710-dcd61c7d42a1 h1:CAVM5ALs/TKIa2ri7WMqge+m5wz/ItuiU6CFUPjZTjA=
|
||||
github.com/schollz/cryptopasta v0.0.0-20170217152710-dcd61c7d42a1/go.mod h1:sM7qObCXSAwGYckYHG4m0hP3PSCBcHmC7/w/kBwcwgM=
|
||||
github.com/schollz/versionedtext v0.0.0-20180523061923-d8ce0957c254 h1:/EgihFrDLhb/x7NLm8cWB7QTquw5gatR+y/jv2gLWsY=
|
||||
github.com/schollz/versionedtext v0.0.0-20180523061923-d8ce0957c254/go.mod h1:116sjYSGDGoVSTUCdO34dA1Yg1ZGbN2jk/aYThLfK60=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20210228213109-c3a9aa474629 h1:86e54L0i3pH3dAIA8OxBbfLrVyhoGpnNk1iJCigAWYs=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20210228213109-c3a9aa474629/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20190704215121-7189cc372560 h1:SpaoQDTgpo2YZkvmr2mtgloFFfPTjtLMlZkQtNAPQik=
|
||||
github.com/shurcooL/go v0.0.0-20190704215121-7189cc372560/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480 h1:KaKXZldeYH73dpQL+Nr38j1r5BgpAYQjYvENOUpIZDQ=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20191220051317-782971ddf21b h1:rBIwpb5ggtqf0uZZY5BPs1sL7njUMM7I8qD2jiou70E=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20191220051317-782971ddf21b/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/octicon v0.0.0-20230705024016-66bff059edb8 h1:W5meM/5DP0Igf+pS3Se363Y2DoDv9LUuZgQ24uG9LNY=
|
||||
github.com/shurcooL/octicon v0.0.0-20230705024016-66bff059edb8/go.mod h1:hWBWTvIJ918VxbNOk2hxQg1/5j1M9yQI1Kp8d9qrOq8=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/wader/gormstore/v2 v2.0.0/go.mod h1:3BgNKFxRdVo2E4pq3e/eiim8qRDZzaveaIcIvu2T8r0=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.mongodb.org/mongo-driver v1.9.0/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
|
||||
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0=
|
||||
gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.0.4/go.mod h1:MEgp8tk2n60cSBCq5iTcPDw3ns8Gs+zOva9EUhkknTs=
|
||||
gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg=
|
||||
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
|
||||
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
497
handlers.go
497
handlers.go
|
@ -1,497 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// "github.com/gin-contrib/static"
|
||||
"github.com/gin-contrib/multitemplate"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/schollz/cowyo/encrypt"
|
||||
)
|
||||
|
||||
func serve(host, port, crt_path, key_path string, TLS bool) {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
router := gin.Default()
|
||||
store := sessions.NewCookieStore([]byte("secret"))
|
||||
router.Use(sessions.Sessions("mysession", store))
|
||||
router.HTMLRender = loadTemplates("index.tmpl")
|
||||
// router.Use(static.Serve("/static/", static.LocalFile("./static", true)))
|
||||
router.GET("/", func(c *gin.Context) {
|
||||
c.Redirect(302, "/"+randomAlliterateCombo())
|
||||
})
|
||||
router.GET("/:page", func(c *gin.Context) {
|
||||
page := c.Param("page")
|
||||
c.Redirect(302, "/"+page+"/edit")
|
||||
})
|
||||
router.GET("/:page/*command", handlePageRequest)
|
||||
router.POST("/update", handlePageUpdate)
|
||||
router.POST("/relinquish", handlePageRelinquish) // relinquish returns the page no matter what (and destroys if nessecary)
|
||||
router.POST("/exists", handlePageExists)
|
||||
router.POST("/prime", handlePrime)
|
||||
router.POST("/lock", handleLock)
|
||||
router.POST("/encrypt", handleEncrypt)
|
||||
router.DELETE("/oldlist", handleClearOldListItems)
|
||||
router.DELETE("/listitem", deleteListItem)
|
||||
|
||||
if TLS {
|
||||
http.ListenAndServeTLS(host+":"+port, crt_path, key_path, router)
|
||||
} else {
|
||||
router.Run(host + ":" + port)
|
||||
}
|
||||
}
|
||||
|
||||
func loadTemplates(list ...string) multitemplate.Render {
|
||||
r := multitemplate.New()
|
||||
|
||||
for _, x := range list {
|
||||
templateString, err := Asset("templates/" + x)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tmplMessage, err := template.New(x).Parse(string(templateString))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
r.Add(x, tmplMessage)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func handlePageRelinquish(c *gin.Context) {
|
||||
type QueryJSON struct {
|
||||
Page string `json:"page"`
|
||||
}
|
||||
var json QueryJSON
|
||||
err := c.BindJSON(&json)
|
||||
if err != nil {
|
||||
log.Trace(err.Error())
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Wrong JSON"})
|
||||
return
|
||||
}
|
||||
if len(json.Page) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Must specify `page`"})
|
||||
return
|
||||
}
|
||||
message := "Relinquished"
|
||||
p := Open(json.Page)
|
||||
name := p.Meta
|
||||
if name == "" {
|
||||
name = json.Page
|
||||
}
|
||||
text := p.Text.GetCurrent()
|
||||
isLocked := p.IsEncrypted
|
||||
isEncrypted := p.IsEncrypted
|
||||
destroyed := p.IsPrimedForSelfDestruct
|
||||
if !p.IsLocked && p.IsPrimedForSelfDestruct {
|
||||
p.Erase()
|
||||
message = "Relinquished and erased"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true,
|
||||
"name": name,
|
||||
"message": message,
|
||||
"text": text,
|
||||
"locked": isLocked,
|
||||
"encrypted": isEncrypted,
|
||||
"destroyed": destroyed})
|
||||
}
|
||||
|
||||
func handlePageRequest(c *gin.Context) {
|
||||
page := c.Param("page")
|
||||
command := c.Param("command")
|
||||
if len(command) < 2 {
|
||||
c.Redirect(302, "/"+page+"/edit")
|
||||
return
|
||||
}
|
||||
// Serve static content from memory
|
||||
if page == "static" {
|
||||
filename := page + command
|
||||
data, err := Asset(filename)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Could not find data")
|
||||
}
|
||||
c.Data(http.StatusOK, contentType(filename), data)
|
||||
return
|
||||
}
|
||||
|
||||
version := c.DefaultQuery("version", "ajksldfjl")
|
||||
p := Open(page)
|
||||
|
||||
// Disallow anything but viewing locked/encrypted pages
|
||||
if (p.IsEncrypted || p.IsLocked) &&
|
||||
(command[0:2] != "/v" && command[0:2] != "/r") {
|
||||
c.Redirect(302, "/"+page+"/view")
|
||||
return
|
||||
}
|
||||
|
||||
// Destroy page if it is opened and primed
|
||||
if p.IsPrimedForSelfDestruct && !p.IsLocked && !p.IsEncrypted {
|
||||
p.Update("*This page has self-destructed. You can not return to it.*\n\n" + p.Text.GetCurrent())
|
||||
p.Erase()
|
||||
command = "/view"
|
||||
}
|
||||
if command == "/erase" {
|
||||
if !p.IsLocked && !p.IsEncrypted {
|
||||
p.Erase()
|
||||
c.Redirect(302, "/"+page+"/edit")
|
||||
} else {
|
||||
c.Redirect(302, "/"+page+"/view")
|
||||
}
|
||||
return
|
||||
}
|
||||
rawText := p.Text.GetCurrent()
|
||||
rawHTML := p.RenderedPage
|
||||
|
||||
// Check to see if an old version is requested
|
||||
versionInt, versionErr := strconv.Atoi(version)
|
||||
if versionErr == nil && versionInt > 0 {
|
||||
versionText, err := p.Text.GetPreviousByTimestamp(int64(versionInt))
|
||||
if err == nil {
|
||||
rawText = versionText
|
||||
rawHTML = GithubMarkdownToHTML(rawText)
|
||||
}
|
||||
}
|
||||
|
||||
// Get history
|
||||
var versionsInt64 []int64
|
||||
var versionsChangeSums []int
|
||||
var versionsText []string
|
||||
if command[0:2] == "/h" {
|
||||
versionsInt64, versionsChangeSums = p.Text.GetMajorSnapshotsAndChangeSums(60) // get snapshots 60 seconds apart
|
||||
versionsText = make([]string, len(versionsInt64))
|
||||
for i, v := range versionsInt64 {
|
||||
versionsText[i] = time.Unix(v/1000000000, 0).Format("Mon Jan 2 15:04:05 MST 2006")
|
||||
}
|
||||
versionsText = reverseSliceString(versionsText)
|
||||
versionsInt64 = reverseSliceInt64(versionsInt64)
|
||||
versionsChangeSums = reverseSliceInt(versionsChangeSums)
|
||||
}
|
||||
|
||||
if command[0:2] == "/r" {
|
||||
c.Writer.Header().Set("Content-Type", contentType(p.Name))
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Max")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
c.Data(200, contentType(p.Name), []byte(rawText))
|
||||
return
|
||||
}
|
||||
log.Debug(command)
|
||||
log.Debug("%v", command[0:2] != "/e" &&
|
||||
command[0:2] != "/v" &&
|
||||
command[0:2] != "/l" &&
|
||||
command[0:2] != "/h")
|
||||
|
||||
var FileNames, FileLastEdited []string
|
||||
var FileSizes, FileNumChanges []int
|
||||
if page == "ls" {
|
||||
command = "/view"
|
||||
FileNames, FileSizes, FileNumChanges, FileLastEdited = DirectoryList()
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||
"EditPage": command[0:2] == "/e", // /edit
|
||||
"ViewPage": command[0:2] == "/v", // /view
|
||||
"ListPage": command[0:2] == "/l", // /list
|
||||
"HistoryPage": command[0:2] == "/h", // /history
|
||||
"DontKnowPage": command[0:2] != "/e" &&
|
||||
command[0:2] != "/v" &&
|
||||
command[0:2] != "/l" &&
|
||||
command[0:2] != "/h",
|
||||
"DirectoryPage": page == "ls",
|
||||
"FileNames": FileNames,
|
||||
"FileSizes": FileSizes,
|
||||
"FileNumChanges": FileNumChanges,
|
||||
"FileLastEdited": FileLastEdited,
|
||||
"Page": page,
|
||||
"RenderedPage": template.HTML([]byte(rawHTML)),
|
||||
"RawPage": rawText,
|
||||
"Versions": versionsInt64,
|
||||
"VersionsText": versionsText,
|
||||
"VersionsChangeSums": versionsChangeSums,
|
||||
"IsLocked": p.IsLocked,
|
||||
"IsEncrypted": p.IsEncrypted,
|
||||
"ListItems": renderList(rawText),
|
||||
"Route": "/" + page + command,
|
||||
"HasDotInName": strings.Contains(page, "."),
|
||||
"RecentlyEdited": getRecentlyEdited(page, c),
|
||||
})
|
||||
}
|
||||
|
||||
func getRecentlyEdited(title string, c *gin.Context) []string {
|
||||
session := sessions.Default(c)
|
||||
var recentlyEdited string
|
||||
v := session.Get("recentlyEdited")
|
||||
editedThings := []string{}
|
||||
if v == nil {
|
||||
recentlyEdited = title
|
||||
} else {
|
||||
editedThings = strings.Split(v.(string), "|||")
|
||||
if !stringInSlice(title, editedThings) {
|
||||
recentlyEdited = v.(string) + "|||" + title
|
||||
} else {
|
||||
recentlyEdited = v.(string)
|
||||
}
|
||||
}
|
||||
session.Set("recentlyEdited", recentlyEdited)
|
||||
session.Save()
|
||||
editedThingsWithoutCurrent := make([]string, len(editedThings))
|
||||
i := 0
|
||||
for _, thing := range editedThings {
|
||||
if thing == title {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(thing, "icon-") {
|
||||
continue
|
||||
}
|
||||
editedThingsWithoutCurrent[i] = thing
|
||||
i++
|
||||
}
|
||||
return editedThingsWithoutCurrent[:i]
|
||||
}
|
||||
|
||||
func handlePageExists(c *gin.Context) {
|
||||
type QueryJSON struct {
|
||||
Page string `json:"page"`
|
||||
}
|
||||
var json QueryJSON
|
||||
err := c.BindJSON(&json)
|
||||
if err != nil {
|
||||
log.Trace(err.Error())
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Wrong JSON", "exists": false})
|
||||
return
|
||||
}
|
||||
p := Open(json.Page)
|
||||
if len(p.Text.GetCurrent()) > 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": json.Page + " found", "exists": true})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": json.Page + " not found", "exists": false})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func handlePageUpdate(c *gin.Context) {
|
||||
type QueryJSON struct {
|
||||
Page string `json:"page"`
|
||||
NewText string `json:"new_text"`
|
||||
IsEncrypted bool `json:"is_encrypted"`
|
||||
IsPrimed bool `json:"is_primed"`
|
||||
Meta string `json:"meta"`
|
||||
}
|
||||
var json QueryJSON
|
||||
err := c.BindJSON(&json)
|
||||
if err != nil {
|
||||
log.Trace(err.Error())
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Wrong JSON"})
|
||||
return
|
||||
}
|
||||
if len(json.NewText) > 100000000 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Too much"})
|
||||
return
|
||||
}
|
||||
if len(json.Page) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Must specify `page`"})
|
||||
return
|
||||
}
|
||||
log.Trace("Update: %v", json)
|
||||
p := Open(json.Page)
|
||||
var message string
|
||||
success := false
|
||||
if p.IsLocked {
|
||||
message = "Locked, must unlock first"
|
||||
} else if p.IsEncrypted {
|
||||
message = "Encrypted, must decrypt first"
|
||||
} else {
|
||||
p.Meta = json.Meta
|
||||
p.Update(json.NewText)
|
||||
if json.IsEncrypted {
|
||||
p.IsEncrypted = true
|
||||
}
|
||||
if json.IsPrimed {
|
||||
p.IsPrimedForSelfDestruct = true
|
||||
}
|
||||
p.Save()
|
||||
message = "Saved"
|
||||
success = true
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": success, "message": message})
|
||||
}
|
||||
|
||||
func handlePrime(c *gin.Context) {
|
||||
type QueryJSON struct {
|
||||
Page string `json:"page"`
|
||||
}
|
||||
var json QueryJSON
|
||||
if c.BindJSON(&json) != nil {
|
||||
c.String(http.StatusBadRequest, "Problem binding keys")
|
||||
return
|
||||
}
|
||||
log.Trace("Update: %v", json)
|
||||
p := Open(json.Page)
|
||||
if p.IsLocked {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Locked"})
|
||||
return
|
||||
} else if p.IsEncrypted {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Encrypted"})
|
||||
return
|
||||
}
|
||||
p.IsPrimedForSelfDestruct = true
|
||||
p.Save()
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Primed"})
|
||||
}
|
||||
|
||||
func handleLock(c *gin.Context) {
|
||||
type QueryJSON struct {
|
||||
Page string `json:"page"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
}
|
||||
|
||||
var json QueryJSON
|
||||
if c.BindJSON(&json) != nil {
|
||||
c.String(http.StatusBadRequest, "Problem binding keys")
|
||||
return
|
||||
}
|
||||
p := Open(json.Page)
|
||||
if p.IsEncrypted {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Encrypted"})
|
||||
return
|
||||
}
|
||||
var message string
|
||||
if p.IsLocked {
|
||||
err2 := CheckPasswordHash(json.Passphrase, p.PassphraseToUnlock)
|
||||
if err2 != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Can't unlock"})
|
||||
return
|
||||
}
|
||||
p.IsLocked = false
|
||||
message = "Unlocked"
|
||||
} else {
|
||||
p.IsLocked = true
|
||||
p.PassphraseToUnlock = HashPassword(json.Passphrase)
|
||||
message = "Locked"
|
||||
}
|
||||
p.Save()
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": message})
|
||||
}
|
||||
|
||||
func handleEncrypt(c *gin.Context) {
|
||||
type QueryJSON struct {
|
||||
Page string `json:"page"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
}
|
||||
|
||||
var json QueryJSON
|
||||
if c.BindJSON(&json) != nil {
|
||||
c.String(http.StatusBadRequest, "Problem binding keys")
|
||||
return
|
||||
}
|
||||
p := Open(json.Page)
|
||||
if p.IsLocked {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Locked"})
|
||||
return
|
||||
}
|
||||
q := Open(json.Page)
|
||||
var message string
|
||||
if p.IsEncrypted {
|
||||
decrypted, err2 := encrypt.DecryptString(p.Text.GetCurrent(), json.Passphrase)
|
||||
if err2 != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Wrong password"})
|
||||
return
|
||||
}
|
||||
q.Erase()
|
||||
q = Open(json.Page)
|
||||
q.Update(decrypted)
|
||||
q.IsEncrypted = false
|
||||
q.IsLocked = p.IsLocked
|
||||
q.IsPrimedForSelfDestruct = p.IsPrimedForSelfDestruct
|
||||
message = "Decrypted"
|
||||
} else {
|
||||
currentText := p.Text.GetCurrent()
|
||||
encrypted, _ := encrypt.EncryptString(currentText, json.Passphrase)
|
||||
q.Erase()
|
||||
q = Open(json.Page)
|
||||
q.Update(encrypted)
|
||||
q.IsEncrypted = true
|
||||
q.IsLocked = p.IsLocked
|
||||
q.IsPrimedForSelfDestruct = p.IsPrimedForSelfDestruct
|
||||
message = "Encrypted"
|
||||
}
|
||||
q.Save()
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": message})
|
||||
}
|
||||
|
||||
func deleteListItem(c *gin.Context) {
|
||||
lineNum, err := strconv.Atoi(c.DefaultQuery("lineNum", "None"))
|
||||
page := c.Query("page") // shortcut for c.Request.URL.Query().Get("lastname")
|
||||
if err == nil {
|
||||
p := Open(page)
|
||||
|
||||
_, listItems := reorderList(p.Text.GetCurrent())
|
||||
newText := p.Text.GetCurrent()
|
||||
for i, lineString := range listItems {
|
||||
// fmt.Println(i, lineString, lineNum)
|
||||
if i+1 == lineNum {
|
||||
// fmt.Println("MATCHED")
|
||||
if strings.Contains(lineString, "~~") == false {
|
||||
// fmt.Println(p.Text, "("+lineString[2:]+"\n"+")", "~~"+lineString[2:]+"~~"+"\n")
|
||||
newText = strings.Replace(newText+"\n", lineString[2:]+"\n", "~~"+strings.TrimSpace(lineString[2:])+"~~"+"\n", 1)
|
||||
} else {
|
||||
newText = strings.Replace(newText+"\n", lineString[2:]+"\n", lineString[4:len(lineString)-2]+"\n", 1)
|
||||
}
|
||||
p.Update(newText)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"message": "Done.",
|
||||
})
|
||||
} else {
|
||||
c.JSON(200, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func handleClearOldListItems(c *gin.Context) {
|
||||
type QueryJSON struct {
|
||||
Page string `json:"page"`
|
||||
}
|
||||
|
||||
var json QueryJSON
|
||||
if c.BindJSON(&json) != nil {
|
||||
c.String(http.StatusBadRequest, "Problem binding keys")
|
||||
return
|
||||
}
|
||||
p := Open(json.Page)
|
||||
if p.IsEncrypted {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Encrypted"})
|
||||
return
|
||||
}
|
||||
if p.IsLocked {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Locked"})
|
||||
return
|
||||
}
|
||||
lines := strings.Split(p.Text.GetCurrent(), "\n")
|
||||
newLines := make([]string, len(lines))
|
||||
newLinesI := 0
|
||||
for _, line := range lines {
|
||||
if strings.Count(line, "~~") != 2 {
|
||||
newLines[newLinesI] = line
|
||||
newLinesI++
|
||||
}
|
||||
}
|
||||
p.Update(strings.Join(newLines[0:newLinesI], "\n"))
|
||||
p.Save()
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Cleared"})
|
||||
}
|
119
main.go
119
main.go
|
@ -2,10 +2,14 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
"github.com/jcelliott/lumber"
|
||||
"github.com/schollz/cowyo/server"
|
||||
|
||||
cli "gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var version string
|
||||
|
@ -18,9 +22,6 @@ func main() {
|
|||
app.Version = version
|
||||
app.Compiled = time.Now()
|
||||
app.Action = func(c *cli.Context) error {
|
||||
if !c.GlobalBool("debug") {
|
||||
turnOffDebugger()
|
||||
}
|
||||
pathToData = c.GlobalString("data")
|
||||
os.MkdirAll(pathToData, 0755)
|
||||
host := c.GlobalString("host")
|
||||
|
@ -38,7 +39,27 @@ func main() {
|
|||
} else {
|
||||
fmt.Printf("\nRunning cowyo server (version %s) at http://%s:%s\n\n", version, host, c.GlobalString("port"))
|
||||
}
|
||||
serve(c.GlobalString("host"), c.GlobalString("port"), c.GlobalString("cert"), c.GlobalString("key"), TLS)
|
||||
|
||||
server.Serve(
|
||||
pathToData,
|
||||
c.GlobalString("host"),
|
||||
c.GlobalString("port"),
|
||||
c.GlobalString("cert"),
|
||||
c.GlobalString("key"),
|
||||
TLS,
|
||||
c.GlobalString("css"),
|
||||
c.GlobalString("default-page"),
|
||||
c.GlobalString("lock"),
|
||||
c.GlobalInt("debounce"),
|
||||
c.GlobalBool("diary"),
|
||||
c.GlobalString("cookie-secret"),
|
||||
c.GlobalString("access-code"),
|
||||
c.GlobalBool("allow-insecure-markup"),
|
||||
c.GlobalBool("allow-file-uploads"),
|
||||
c.GlobalUint("max-upload-mb"),
|
||||
c.GlobalUint("max-document-length"),
|
||||
logger(c.GlobalBool("debug")),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
app.Flags = []cli.Flag{
|
||||
|
@ -72,10 +93,62 @@ func main() {
|
|||
Value: "",
|
||||
Usage: "absolute path to SSL private key",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "css",
|
||||
Value: "",
|
||||
Usage: "use a custom CSS file",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "default-page",
|
||||
Value: "",
|
||||
Usage: "show default-page/read instead of editing (default: show random editing)",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "allow-insecure-markup",
|
||||
Usage: "Skip HTML sanitization",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "lock",
|
||||
Value: "",
|
||||
Usage: "password to lock editing all files (default: all pages unlocked)",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "debounce",
|
||||
Value: 500,
|
||||
Usage: "debounce time for saving data, in milliseconds",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug, d",
|
||||
Usage: "turn on debugging",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "diary",
|
||||
Usage: "turn diary mode (doing New will give a timestamped page)",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "access-code",
|
||||
Value: "",
|
||||
Usage: "Secret code to login with before accessing any wiki stuff",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "cookie-secret",
|
||||
Value: "secret",
|
||||
Usage: "random data to use for cookies; changing it will invalidate all sessions",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "allow-file-uploads",
|
||||
Usage: "Enable file uploads",
|
||||
},
|
||||
cli.UintFlag{
|
||||
Name: "max-upload-mb",
|
||||
Value: 2,
|
||||
Usage: "Largest file upload (in mb) allowed",
|
||||
},
|
||||
cli.UintFlag{
|
||||
Name: "max-document-length",
|
||||
Value: 100000000,
|
||||
Usage: "Largest wiki page (in characters) allowed",
|
||||
},
|
||||
}
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
|
@ -83,9 +156,6 @@ func main() {
|
|||
Aliases: []string{"m"},
|
||||
Usage: "migrate from the old cowyo",
|
||||
Action: func(c *cli.Context) error {
|
||||
if !c.GlobalBool("debug") {
|
||||
turnOffDebugger()
|
||||
}
|
||||
pathToData = c.GlobalString("data")
|
||||
pathToOldData := c.GlobalString("olddata")
|
||||
if len(pathToOldData) == 0 {
|
||||
|
@ -97,12 +167,43 @@ func main() {
|
|||
fmt.Printf("Can not find '%s', does it exist?", pathToOldData)
|
||||
return nil
|
||||
}
|
||||
migrate(pathToOldData, pathToData)
|
||||
server.Migrate(pathToOldData, pathToData, logger(c.GlobalBool("debug")))
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
// GetLocalIP returns the local ip address
|
||||
func GetLocalIP() string {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
bestIP := ""
|
||||
for _, address := range addrs {
|
||||
// check the address type and if it is not a loopback the display it
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
return ipnet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestIP
|
||||
}
|
||||
|
||||
// exists returns whether the given file or directory exists or not
|
||||
func exists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func logger(debug bool) *lumber.ConsoleLogger {
|
||||
if !debug {
|
||||
return lumber.NewConsoleLogger(lumber.WARN)
|
||||
}
|
||||
return lumber.NewConsoleLogger(lumber.TRACE)
|
||||
|
||||
}
|
||||
|
|
25
migrate.go
25
migrate.go
|
@ -1,25 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
)
|
||||
|
||||
func migrate(pathToOldData, pathToData string) error {
|
||||
files, _ := ioutil.ReadDir(pathToOldData)
|
||||
for _, f := range files {
|
||||
fmt.Printf("Migrating %s", f.Name())
|
||||
p := Open(f.Name())
|
||||
bData, err := ioutil.ReadFile(path.Join(pathToOldData, f.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.Update(string(bData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Save()
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
[default]
|
||||
dataDir = "root_data/${HOST}"
|
||||
maxDocumentLength = 100000000
|
||||
|
||||
# Specify multiple times to change keys without expiring sessions
|
||||
[[default.CookieKeys]]
|
||||
authenticateBase64 = "RpW4LjGCPNOx75G8DrywmzlEHLB/ISXCAAayZ47Ifkc="
|
||||
encryptBase64 = "ofCKkrfosQb5T4cvz7R5IMP4BQUDHOPsLSMZZy2CUOA="
|
||||
|
||||
[[sites]]
|
||||
host = "nerdy.party"
|
||||
dataDir = "somewhere else"
|
||||
# theme = "custom.css" # TODO: Theme support. Would prefer to move to a complete directory replacement.
|
||||
defaultPage = "welcome"
|
||||
allowInsecureMarkup = true
|
||||
lock = "1234"
|
||||
debounceSave = 600
|
||||
diary = true
|
||||
accessCode = "correct horse battery staple"
|
||||
fileUploadsAllowed = true
|
||||
maxFileUploadMb = 6
|
||||
port = 8090
|
||||
|
||||
#[sites.TLS]
|
||||
# TODO: ACME support eg letsencrypt
|
||||
#certPath = "path.crt"
|
||||
#keyPath = "path.key"
|
||||
#port = 8443
|
||||
|
||||
[[sites]]
|
||||
host = "cowyo.com"
|
||||
allowInsecureMarkup = false
|
||||
fileUploadsAllowed = false
|
||||
port = 8090
|
||||
|
||||
# Catchall config
|
||||
[[sites]]
|
||||
host = "*"
|
||||
port = 8100
|
||||
cookieSecret = "ASADFGKLJSH+4t4cC2X3f7GzsLZ+wtST67qoLuErpugJz06ZIpdDHEjcMxR+XOLM"
|
106
page.go
106
page.go
|
@ -1,106 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/schollz/versionedtext"
|
||||
)
|
||||
|
||||
// Page is the basic struct
|
||||
type Page struct {
|
||||
Name string
|
||||
Text versionedtext.VersionedText
|
||||
Meta string
|
||||
RenderedPage string
|
||||
IsLocked bool
|
||||
PassphraseToUnlock string
|
||||
IsEncrypted bool
|
||||
IsPrimedForSelfDestruct bool
|
||||
}
|
||||
|
||||
func Open(name string) (p *Page) {
|
||||
p = new(Page)
|
||||
p.Name = name
|
||||
p.Text = versionedtext.NewVersionedText("")
|
||||
p.Render()
|
||||
bJSON, err := ioutil.ReadFile(path.Join(pathToData, encodeToBase32(strings.ToLower(name))+".json"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(bJSON, &p)
|
||||
if err != nil {
|
||||
p = new(Page)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func DirectoryList() (names []string, lengths []int, numchanges []int, lastEdited []string) {
|
||||
files, _ := ioutil.ReadDir(pathToData)
|
||||
names = make([]string, len(files))
|
||||
lengths = make([]int, len(files))
|
||||
numchanges = make([]int, len(files))
|
||||
lastEdited = make([]string, len(files))
|
||||
for i, f := range files {
|
||||
names[i] = DecodeFileName(f.Name())
|
||||
p := Open(names[i])
|
||||
lengths[i] = len(p.Text.GetCurrent())
|
||||
numchanges[i] = p.Text.NumEdits()
|
||||
lastEdited[i] = time.Unix(p.Text.LastEditTime()/1000000000, 0).Format("Mon Jan 2 15:04:05 MST 2006")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func DecodeFileName(s string) string {
|
||||
s2, _ := decodeFromBase32(strings.Split(s, ".")[0])
|
||||
return s2
|
||||
}
|
||||
|
||||
// Update cleans the text and updates the versioned text
|
||||
// and generates a new render
|
||||
func (p *Page) Update(newText string) error {
|
||||
// Trim space from end
|
||||
newText = strings.TrimRight(newText, "\n\t ")
|
||||
|
||||
// Update the versioned text
|
||||
p.Text.Update(newText)
|
||||
|
||||
// Render the new page
|
||||
p.Render()
|
||||
|
||||
return p.Save()
|
||||
}
|
||||
|
||||
func (p *Page) Render() {
|
||||
if p.IsEncrypted {
|
||||
p.RenderedPage = "<code>" + p.Text.GetCurrent() + "</code>"
|
||||
return
|
||||
}
|
||||
|
||||
// Convert [[page]] to [page](/page/view)
|
||||
r, _ := regexp.Compile("\\[\\[(.*?)\\]\\]")
|
||||
currentText := p.Text.GetCurrent()
|
||||
for _, s := range r.FindAllString(currentText, -1) {
|
||||
currentText = strings.Replace(currentText, s, "["+s[2:len(s)-2]+"](/"+s[2:len(s)-2]+"/view)", 1)
|
||||
}
|
||||
p.Text.Update(currentText)
|
||||
p.RenderedPage = MarkdownToHtml(p.Text.GetCurrent())
|
||||
}
|
||||
|
||||
func (p *Page) Save() error {
|
||||
bJSON, err := json.MarshalIndent(p, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(path.Join(pathToData, encodeToBase32(strings.ToLower(p.Name))+".json"), bJSON, 0644)
|
||||
}
|
||||
|
||||
func (p *Page) Erase() error {
|
||||
log.Trace("Erasing " + p.Name)
|
||||
return os.Remove(path.Join(pathToData, encodeToBase32(strings.ToLower(p.Name))+".json"))
|
||||
}
|
49
page_test.go
49
page_test.go
|
@ -1,49 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
// "fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListFiles(t *testing.T) {
|
||||
pathToData = "testdata"
|
||||
os.MkdirAll(pathToData, 0755)
|
||||
defer os.RemoveAll(pathToData)
|
||||
p := Open("testpage")
|
||||
p.Update("Some data")
|
||||
p = Open("testpage2")
|
||||
p.Update("A different bunch of data")
|
||||
p = Open("testpage3")
|
||||
p.Update("Not much else")
|
||||
n, l, _, _ := DirectoryList()
|
||||
if strings.Join(n, " ") != "testpage testpage2 testpage3" {
|
||||
t.Errorf("Names: %s, Lengths: %d", n, l)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneral(t *testing.T) {
|
||||
pathToData = "testdata"
|
||||
os.MkdirAll(pathToData, 0755)
|
||||
defer os.RemoveAll(pathToData)
|
||||
p := Open("testpage")
|
||||
err := p.Update("**bold**")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if strings.TrimSpace(p.RenderedPage) != "<p><strong>bold</strong></p>" {
|
||||
t.Errorf("Did not render: '%s'", p.RenderedPage)
|
||||
}
|
||||
err = p.Update("**bold** and *italic*")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
p.Save()
|
||||
|
||||
p2 := Open("testpage")
|
||||
if strings.TrimSpace(p2.RenderedPage) != "<p><strong>bold</strong> and <em>italic</em></p>" {
|
||||
t.Errorf("Did not render: '%s'", p2.RenderedPage)
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,10 @@
|
|||
// +build debug
|
||||
|
||||
package server
|
||||
|
||||
import "github.com/jcelliott/lumber"
|
||||
|
||||
func init() {
|
||||
hotTemplateReloading = true
|
||||
LogLevel = lumber.TRACE
|
||||
}
|
|
@ -0,0 +1,889 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
secretRequired "github.com/danielheath/gin-teeny-security"
|
||||
"github.com/gin-contrib/multitemplate"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jcelliott/lumber"
|
||||
"github.com/schollz/cowyo/encrypt"
|
||||
)
|
||||
|
||||
const minutesToUnlock = 10.0
|
||||
|
||||
type Site struct {
|
||||
PathToData string
|
||||
Css []byte
|
||||
DefaultPage string
|
||||
DefaultPassword string
|
||||
Debounce int
|
||||
Diary bool
|
||||
SessionStore cookie.Store
|
||||
SecretCode string
|
||||
AllowInsecure bool
|
||||
Fileuploads bool
|
||||
MaxUploadSize uint
|
||||
Logger *lumber.ConsoleLogger
|
||||
MaxDocumentSize uint // in runes; about a 10mb limit by default
|
||||
saveMut sync.Mutex
|
||||
sitemapUpToDate bool // TODO this makes everything use a pointer
|
||||
}
|
||||
|
||||
func (s *Site) defaultLock() string {
|
||||
if s.DefaultPassword == "" {
|
||||
return ""
|
||||
}
|
||||
return HashPassword(s.DefaultPassword)
|
||||
}
|
||||
|
||||
var hotTemplateReloading bool
|
||||
var LogLevel int = lumber.WARN
|
||||
|
||||
func Serve(
|
||||
filepathToData,
|
||||
host,
|
||||
port,
|
||||
crt_path,
|
||||
key_path string,
|
||||
TLS bool,
|
||||
cssFile string,
|
||||
defaultPage string,
|
||||
defaultPassword string,
|
||||
debounce int,
|
||||
diary bool,
|
||||
secret string,
|
||||
secretCode string,
|
||||
allowInsecure bool,
|
||||
fileuploads bool,
|
||||
maxUploadSize uint,
|
||||
maxDocumentSize uint,
|
||||
logger *lumber.ConsoleLogger,
|
||||
) {
|
||||
var customCSS []byte
|
||||
// collect custom CSS
|
||||
if len(cssFile) > 0 {
|
||||
var errRead error
|
||||
customCSS, errRead = ioutil.ReadFile(cssFile)
|
||||
if errRead != nil {
|
||||
fmt.Println(errRead)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Loaded CSS file, %d bytes\n", len(customCSS))
|
||||
}
|
||||
|
||||
router := Site{
|
||||
PathToData: filepathToData,
|
||||
Css: customCSS,
|
||||
DefaultPage: defaultPage,
|
||||
DefaultPassword: defaultPassword,
|
||||
Debounce: debounce,
|
||||
Diary: diary,
|
||||
SessionStore: cookie.NewStore([]byte(secret)),
|
||||
SecretCode: secretCode,
|
||||
AllowInsecure: allowInsecure,
|
||||
Fileuploads: fileuploads,
|
||||
MaxUploadSize: maxUploadSize,
|
||||
Logger: logger,
|
||||
MaxDocumentSize: maxDocumentSize,
|
||||
}.Router()
|
||||
|
||||
if TLS {
|
||||
http.ListenAndServeTLS(host+":"+port, crt_path, key_path, router)
|
||||
} else {
|
||||
panic(router.Run(host + ":" + port))
|
||||
}
|
||||
}
|
||||
|
||||
func (s Site) Router() *gin.Engine {
|
||||
if s.Logger == nil {
|
||||
s.Logger = lumber.NewConsoleLogger(lumber.TRACE)
|
||||
}
|
||||
|
||||
if hotTemplateReloading {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
router := gin.Default()
|
||||
router.SetFuncMap(template.FuncMap{
|
||||
"sniffContentType": s.sniffContentType,
|
||||
})
|
||||
|
||||
if hotTemplateReloading {
|
||||
router.LoadHTMLGlob("templates/*.tmpl")
|
||||
} else {
|
||||
router.HTMLRender = s.loadTemplates("index.tmpl")
|
||||
}
|
||||
|
||||
router.Use(sessions.Sessions("_session", s.SessionStore))
|
||||
if s.SecretCode != "" {
|
||||
cfg := &secretRequired.Config{
|
||||
Secret: s.SecretCode,
|
||||
Path: "/login/",
|
||||
RequireAuth: func(c *gin.Context) bool {
|
||||
page := c.Param("page")
|
||||
cmd := c.Param("command")
|
||||
|
||||
if page == "sitemap.xml" || page == "favicon.ico" || page == "static" || page == "uploads" {
|
||||
return false // no auth for these
|
||||
}
|
||||
|
||||
if page != "" && cmd == "/read" {
|
||||
p := s.Open(page)
|
||||
if p != nil && p.IsPublished {
|
||||
return false // Published pages don't require auth.
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
router.Use(cfg.Middleware)
|
||||
}
|
||||
|
||||
// router.Use(static.Serve("/static/", static.LocalFile("./static", true)))
|
||||
router.GET("/", func(c *gin.Context) {
|
||||
if s.DefaultPage != "" {
|
||||
c.Redirect(302, "/"+s.DefaultPage+"/read")
|
||||
} else {
|
||||
c.Redirect(302, "/"+randomAlliterateCombo())
|
||||
}
|
||||
})
|
||||
|
||||
router.POST("/uploads", s.handleUpload)
|
||||
|
||||
router.GET("/:page", func(c *gin.Context) {
|
||||
page := c.Param("page")
|
||||
c.Redirect(302, "/"+page+"/")
|
||||
})
|
||||
router.GET("/:page/*command", s.handlePageRequest)
|
||||
router.POST("/update", s.handlePageUpdate)
|
||||
router.POST("/relinquish", s.handlePageRelinquish) // relinquish returns the page no matter what (and destroys if nessecary)
|
||||
router.POST("/exists", s.handlePageExists)
|
||||
router.POST("/prime", s.handlePrime)
|
||||
router.POST("/lock", s.handleLock)
|
||||
router.POST("/publish", s.handlePublish)
|
||||
router.POST("/encrypt", s.handleEncrypt)
|
||||
router.DELETE("/oldlist", s.handleClearOldListItems)
|
||||
router.DELETE("/listitem", s.deleteListItem)
|
||||
|
||||
// start long-processes as threads
|
||||
go s.thread_SiteMap()
|
||||
|
||||
// Allow iframe/scripts in markup?
|
||||
allowInsecureHtml = s.AllowInsecure
|
||||
return router
|
||||
}
|
||||
|
||||
func (s *Site) loadTemplates(list ...string) multitemplate.Render {
|
||||
r := multitemplate.New()
|
||||
|
||||
for _, x := range list {
|
||||
templateString, err := Asset("templates/" + x)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tmplMessage, err := template.New(x).Funcs(template.FuncMap{
|
||||
"sniffContentType": s.sniffContentType,
|
||||
}).Parse(string(templateString))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
r.Add(x, tmplMessage)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func pageIsLocked(p *Page, c *gin.Context) bool {
|
||||
// it is easier to reason about when the page is actually unlocked
|
||||
var unlocked = !p.IsLocked ||
|
||||
(p.IsLocked && p.UnlockedFor == getSetSessionID(c))
|
||||
return !unlocked
|
||||
}
|
||||
|
||||
func (s *Site) handlePageRelinquish(c *gin.Context) {
|
||||
type QueryJSON struct {
|
||||
Page string `json:"page"`
|
||||
}
|
||||
var json QueryJSON
|
||||
err := c.BindJSON(&json)
|
||||
if err != nil {
|
||||
s.Logger.Trace(err.Error())
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Wrong JSON"})
|
||||
return
|
||||
}
|
||||
if len(json.Page) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Must specify `page`"})
|
||||
return
|
||||
}
|
||||
message := "Relinquished"
|
||||
p := s.Open(json.Page)
|
||||
name := p.Meta
|
||||
if name == "" {
|
||||
name = json.Page
|
||||
}
|
||||
text := p.Text.GetCurrent()
|
||||
isLocked := pageIsLocked(p, c)
|
||||
isEncrypted := p.IsEncrypted
|
||||
destroyed := p.IsPrimedForSelfDestruct
|
||||
if !isLocked && p.IsPrimedForSelfDestruct {
|
||||
p.Erase()
|
||||
message = "Relinquished and erased"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true,
|
||||
"name": name,
|
||||
"message": message,
|
||||
"text": text,
|
||||
"locked": isLocked,
|
||||
"encrypted": isEncrypted,
|
||||
"destroyed": destroyed})
|
||||
}
|
||||
|
||||
func getSetSessionID(c *gin.Context) (sid string) {
|
||||
var (
|
||||
session = sessions.Default(c)
|
||||
v = session.Get("sid")
|
||||
)
|
||||
if v != nil {
|
||||
sid = v.(string)
|
||||
}
|
||||
if v == nil || sid == "" {
|
||||
sid = RandStringBytesMaskImprSrc(8)
|
||||
session.Set("sid", sid)
|
||||
session.Save()
|
||||
}
|
||||
return sid
|
||||
}
|
||||
|
||||
func (s *Site) thread_SiteMap() {
|
||||
for {
|
||||
if !s.sitemapUpToDate {
|
||||
s.Logger.Info("Generating sitemap...")
|
||||
s.sitemapUpToDate = true
|
||||
ioutil.WriteFile(path.Join(s.PathToData, "sitemap.xml"), []byte(s.generateSiteMap()), 0644)
|
||||
s.Logger.Info("..finished generating sitemap")
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Site) generateSiteMap() (sitemap string) {
|
||||
files, _ := ioutil.ReadDir(s.PathToData)
|
||||
lastEdited := make([]string, len(files))
|
||||
names := make([]string, len(files))
|
||||
i := 0
|
||||
for _, f := range files {
|
||||
names[i] = DecodeFileName(f.Name())
|
||||
p := s.Open(names[i])
|
||||
if p.IsPublished {
|
||||
lastEdited[i] = time.Unix(p.Text.LastEditTime()/1000000000, 0).Format("2006-01-02")
|
||||
i++
|
||||
}
|
||||
}
|
||||
names = names[:i]
|
||||
lastEdited = lastEdited[:i]
|
||||
sitemap = ""
|
||||
for i := range names {
|
||||
sitemap += fmt.Sprintf(`
|
||||
<url>
|
||||
<loc>{{ .Request.Host }}/%s/read</loc>
|
||||
<lastmod>%s</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
`, names[i], lastEdited[i])
|
||||
}
|
||||
sitemap += "</urlset>"
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Site) handlePageRequest(c *gin.Context) {
|
||||
page := c.Param("page")
|
||||
command := c.Param("command")
|
||||
|
||||
if page == "sitemap.xml" {
|
||||
siteMap, err := ioutil.ReadFile(path.Join(s.PathToData, "sitemap.xml"))
|
||||
if err != nil {
|
||||
c.Data(http.StatusInternalServerError, contentType("sitemap.xml"), []byte(""))
|
||||
} else {
|
||||
fmt.Fprintln(c.Writer, `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`)
|
||||
template.Must(template.New("sitemap").Parse(string(siteMap))).Execute(c.Writer, c)
|
||||
}
|
||||
return
|
||||
} else if page == "favicon.ico" {
|
||||
data, _ := Asset("/static/img/cowyo/favicon.ico")
|
||||
c.Data(http.StatusOK, contentType("/static/img/cowyo/favicon.ico"), data)
|
||||
return
|
||||
} else if page == "static" {
|
||||
filename := page + command
|
||||
var data []byte
|
||||
if filename == "static/css/custom.css" {
|
||||
data = s.Css
|
||||
} else {
|
||||
var errAssset error
|
||||
data, errAssset = Asset(filename)
|
||||
if errAssset != nil {
|
||||
c.String(http.StatusInternalServerError, "Could not find data")
|
||||
}
|
||||
}
|
||||
c.Data(http.StatusOK, contentType(filename), data)
|
||||
return
|
||||
} else if page == "uploads" {
|
||||
if len(command) == 0 || command == "/" || command == "/edit" {
|
||||
if !s.Fileuploads {
|
||||
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Uploads are disabled on this server"))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
command = command[1:]
|
||||
if !strings.HasSuffix(command, ".upload") {
|
||||
command = command + ".upload"
|
||||
}
|
||||
pathname := path.Join(s.PathToData, command)
|
||||
|
||||
if allowInsecureHtml {
|
||||
c.Header(
|
||||
"Content-Disposition",
|
||||
`inline; filename="`+c.DefaultQuery("filename", "upload")+`"`,
|
||||
)
|
||||
} else {
|
||||
// Prevent malicious html uploads by forcing type to plaintext and 'download-instead-of-view'
|
||||
c.Header("Content-Type", "text/plain")
|
||||
c.Header(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="`+c.DefaultQuery("filename", "upload")+`"`,
|
||||
)
|
||||
}
|
||||
c.File(pathname)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
p := s.Open(page)
|
||||
if len(command) < 2 {
|
||||
if p.IsPublished {
|
||||
c.Redirect(302, "/"+page+"/read")
|
||||
} else {
|
||||
c.Redirect(302, "/"+page+"/edit")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// use the default lock
|
||||
if s.defaultLock() != "" && p.IsNew() {
|
||||
p.IsLocked = true
|
||||
p.PassphraseToUnlock = s.defaultLock()
|
||||
}
|
||||
|
||||
version := c.DefaultQuery("version", "ajksldfjl")
|
||||
isLocked := pageIsLocked(p, c)
|
||||
|
||||
// Disallow anything but viewing locked/encrypted pages
|
||||
if (p.IsEncrypted || isLocked) &&
|
||||
(command[0:2] != "/v" && command[0:2] != "/r") {
|
||||
c.Redirect(302, "/"+page+"/view")
|
||||
return
|
||||
}
|
||||
|
||||
// Destroy page if it is opened and primed
|
||||
if p.IsPrimedForSelfDestruct && !isLocked && !p.IsEncrypted {
|
||||
p.Update("<center><em>This page has self-destructed. You cannot return to it.</em></center>\n\n" + p.Text.GetCurrent())
|
||||
p.Erase()
|
||||
if p.IsPublished {
|
||||
command = "/read"
|
||||
} else {
|
||||
command = "/view"
|
||||
}
|
||||
}
|
||||
if command == "/erase" {
|
||||
if !isLocked && !p.IsEncrypted {
|
||||
p.Erase()
|
||||
c.Redirect(302, "/"+page+"/edit")
|
||||
} else {
|
||||
c.Redirect(302, "/"+page+"/view")
|
||||
}
|
||||
return
|
||||
}
|
||||
rawText := p.Text.GetCurrent()
|
||||
rawHTML := p.RenderedPage
|
||||
|
||||
// Check to see if an old version is requested
|
||||
versionInt, versionErr := strconv.Atoi(version)
|
||||
if versionErr == nil && versionInt > 0 {
|
||||
versionText, err := p.Text.GetPreviousByTimestamp(int64(versionInt))
|
||||
if err == nil {
|
||||
rawText = versionText
|
||||
rawHTML = GithubMarkdownToHTML(rawText)
|
||||
}
|
||||
}
|
||||
|
||||
// Get history
|
||||
var versionsInt64 []int64
|
||||
var versionsChangeSums []int
|
||||
var versionsText []string
|
||||
if command[0:2] == "/h" {
|
||||
versionsInt64, versionsChangeSums = p.Text.GetMajorSnapshotsAndChangeSums(60) // get snapshots 60 seconds apart
|
||||
versionsText = make([]string, len(versionsInt64))
|
||||
for i, v := range versionsInt64 {
|
||||
versionsText[i] = time.Unix(v/1000000000, 0).Format("Mon Jan 2 15:04:05 MST 2006")
|
||||
}
|
||||
versionsText = reverseSliceString(versionsText)
|
||||
versionsInt64 = reverseSliceInt64(versionsInt64)
|
||||
versionsChangeSums = reverseSliceInt(versionsChangeSums)
|
||||
}
|
||||
|
||||
if len(command) > 3 && command[0:3] == "/ra" {
|
||||
c.Writer.Header().Set("Content-Type", "text/plain")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Max")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
c.Data(200, contentType(p.Name), []byte(rawText))
|
||||
return
|
||||
}
|
||||
|
||||
var DirectoryEntries []os.FileInfo
|
||||
if page == "ls" {
|
||||
command = "/view"
|
||||
DirectoryEntries = s.DirectoryList()
|
||||
}
|
||||
if page == "uploads" {
|
||||
command = "/view"
|
||||
var err error
|
||||
DirectoryEntries, err = s.UploadList()
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// swap out /view for /read if it is published
|
||||
if p.IsPublished {
|
||||
rawHTML = strings.Replace(rawHTML, "/view", "/read", -1)
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||
"EditPage": command[0:2] == "/e", // /edit
|
||||
"ViewPage": command[0:2] == "/v", // /view
|
||||
"ListPage": command[0:2] == "/l", // /list
|
||||
"HistoryPage": command[0:2] == "/h", // /history
|
||||
"ReadPage": command[0:2] == "/r", // /history
|
||||
"DontKnowPage": command[0:2] != "/e" &&
|
||||
command[0:2] != "/v" &&
|
||||
command[0:2] != "/l" &&
|
||||
command[0:2] != "/r" &&
|
||||
command[0:2] != "/h",
|
||||
"DirectoryPage": page == "ls" || page == "uploads",
|
||||
"UploadPage": page == "uploads",
|
||||
"DirectoryEntries": DirectoryEntries,
|
||||
"Page": page,
|
||||
"RenderedPage": template.HTML([]byte(rawHTML)),
|
||||
"RawPage": rawText,
|
||||
"Versions": versionsInt64,
|
||||
"VersionsText": versionsText,
|
||||
"VersionsChangeSums": versionsChangeSums,
|
||||
"IsLocked": isLocked,
|
||||
"IsEncrypted": p.IsEncrypted,
|
||||
"ListItems": renderList(rawText),
|
||||
"Route": "/" + page + command,
|
||||
"HasDotInName": strings.Contains(page, "."),
|
||||
"RecentlyEdited": getRecentlyEdited(page, c),
|
||||
"IsPublished": p.IsPublished,
|
||||
"CustomCSS": len(s.Css) > 0,
|
||||
"Debounce": s.Debounce,
|
||||
"DiaryMode": s.Diary,
|
||||
"Date": time.Now().Format("2006-01-02"),
|
||||
"UnixTime": time.Now().Unix(),
|
||||
"ChildPageNames": p.ChildPageNames(),
|
||||
"AllowFileUploads": s.Fileuploads,
|
||||
"MaxUploadMB": s.MaxUploadSize,
|
||||
})
|
||||
}
|
||||
|
||||
func getRecentlyEdited(title string, c *gin.Context) []string {
|
||||
session := sessions.Default(c)
|
||||
var recentlyEdited string
|
||||
v := session.Get("recentlyEdited")
|
||||
editedThings := []string{}
|
||||
if v == nil {
|
||||
recentlyEdited = title
|
||||
} else {
|
||||
editedThings = strings.Split(v.(string), "|||")
|
||||
if !stringInSlice(title, editedThings) {
|
||||
recentlyEdited = v.(string) + "|||" + title
|
||||
} else {
|
||||
recentlyEdited = v.(string)
|
||||
}
|
||||
}
|
||||
session.Set("recentlyEdited", recentlyEdited)
|
||||
session.Save()
|
||||
editedThingsWithoutCurrent := make([]string, len(editedThings))
|
||||
i := 0
|
||||
for _, thing := range editedThings {
|
||||
if thing == title {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(thing, "icon-") {
|
||||
continue
|
||||
}
|
||||
editedThingsWithoutCurrent[i] = thing
|
||||
i++
|
||||
}
|
||||
return editedThingsWithoutCurrent[:i]
|
||||
}
|
||||
|
||||
func (s *Site) handlePageExists(c *gin.Context) {
|
||||
type QueryJSON struct {
|
||||
Page string `json:"page"`
|
||||
}
|
||||
var json QueryJSON
|
||||
err := c.BindJSON(&json)
|
||||
if err != nil {
|
||||
s.Logger.Trace(err.Error())
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Wrong JSON", "exists": false})
|
||||
return
|
||||
}
|
||||
p := s.Open(json.Page)
|
||||
if len(p.Text.GetCurrent()) > 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": json.Page + " found", "exists": true})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": json.Page + " not found", "exists": false})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *Site) handlePageUpdate(c *gin.Context) {
|
||||
type QueryJSON struct {
|
||||
Page string `json:"page"`
|
||||
NewText string `json:"new_text"`
|
||||
FetchedAt int64 `json:"fetched_at"`
|
||||
IsEncrypted bool `json:"is_encrypted"`
|
||||
IsPrimed bool `json:"is_primed"`
|
||||
Meta string `json:"meta"`
|
||||
}
|
||||
var json QueryJSON
|
||||
err := c.BindJSON(&json)
|
||||
if err != nil {
|
||||
s.Logger.Trace(err.Error())
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Wrong JSON"})
|
||||
return
|
||||
}
|
||||
if uint(len(json.NewText)) > s.MaxDocumentSize {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Too much"})
|
||||
return
|
||||
}
|
||||
if len(json.Page) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Must specify `page`"})
|
||||
return
|
||||
}
|
||||
s.Logger.Trace("Update: %v", json)
|
||||
p := s.Open(json.Page)
|
||||
var (
|
||||
message string
|
||||
sinceLastEdit = time.Since(p.LastEditTime())
|
||||
)
|
||||
success := false
|
||||
if pageIsLocked(p, c) {
|
||||
if sinceLastEdit < minutesToUnlock {
|
||||
message = "This page is being edited by someone else"
|
||||
} else {
|
||||
// here what might have happened is that two people unlock without
|
||||
// editing thus they both suceeds but only one is able to edit
|
||||
message = "Locked, must unlock first"
|
||||
}
|
||||
} else if p.IsEncrypted {
|
||||
message = "Encrypted, must decrypt first"
|
||||
} else if json.FetchedAt > 0 && p.LastEditUnixTime() > json.FetchedAt {
|
||||
message = "Refusing to overwrite others work"
|
||||
} else {
|
||||
p.Meta = json.Meta
|
||||
p.Update(json.NewText)
|
||||
if json.IsEncrypted {
|
||||
p.IsEncrypted = true
|
||||
}
|
||||
if json.IsPrimed {
|
||||
p.IsPrimedForSelfDestruct = true
|
||||
}
|
||||
p.Save()
|
||||
message = "Saved"
|
||||
if p.IsPublished {
|
||||
s.sitemapUpToDate = false
|
||||
}
|
||||
success = true
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": success, "message": message, "unix_time": time.Now().Unix()})
|
||||
}
|
||||
|
||||
func (s *Site) handlePrime(c *gin.Context) {
|
||||
type QueryJSON struct {
|
||||
Page string `json:"page"`
|
||||
}
|
||||
var json QueryJSON
|
||||
if c.BindJSON(&json) != nil {
|
||||
c.String(http.StatusBadRequest, "Problem binding keys")
|
||||
return
|
||||
}
|
||||
s.Logger.Trace("Update: %v", json)
|
||||
p := s.Open(json.Page)
|
||||
if pageIsLocked(p, c) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Locked"})
|
||||
return
|
||||
} else if p.IsEncrypted {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Encrypted"})
|
||||
return
|
||||
}
|
||||
p.IsPrimedForSelfDestruct = true
|
||||
p.Save()
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Primed"})
|
||||
}
|
||||
|
||||
func (s *Site) handleLock(c *gin.Context) {
|
||||
type QueryJSON struct {
|
||||
Page string `json:"page"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
}
|
||||
|
||||
var json QueryJSON
|
||||
if c.BindJSON(&json) != nil {
|
||||
c.String(http.StatusBadRequest, "Problem binding keys")
|
||||
return
|
||||
}
|
||||
p := s.Open(json.Page)
|
||||
if s.defaultLock() != "" && p.IsNew() {
|
||||
p.IsLocked = true // IsLocked was replaced by variable wrt Context
|
||||
p.PassphraseToUnlock = s.defaultLock()
|
||||
}
|
||||
|
||||
if p.IsEncrypted {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Encrypted"})
|
||||
return
|
||||
}
|
||||
var (
|
||||
message string
|
||||
sessionID = getSetSessionID(c)
|
||||
sinceLastEdit = time.Since(p.LastEditTime())
|
||||
)
|
||||
|
||||
// both lock/unlock ends here on locked&timeout combination
|
||||
if p.IsLocked &&
|
||||
p.UnlockedFor != sessionID &&
|
||||
p.UnlockedFor != "" &&
|
||||
sinceLastEdit.Minutes() < minutesToUnlock {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": fmt.Sprintf("This page is being edited by someone else! Will unlock automatically %2.0f minutes after the last change.", minutesToUnlock-sinceLastEdit.Minutes()),
|
||||
})
|
||||
return
|
||||
}
|
||||
if !pageIsLocked(p, c) {
|
||||
p.IsLocked = true
|
||||
p.PassphraseToUnlock = HashPassword(json.Passphrase)
|
||||
p.UnlockedFor = ""
|
||||
message = "Locked"
|
||||
} else {
|
||||
err2 := CheckPasswordHash(json.Passphrase, p.PassphraseToUnlock)
|
||||
if err2 != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Can't unlock"})
|
||||
return
|
||||
}
|
||||
p.UnlockedFor = sessionID
|
||||
message = "Unlocked only for you"
|
||||
}
|
||||
p.Save()
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": message})
|
||||
}
|
||||
|
||||
func (s *Site) handlePublish(c *gin.Context) {
|
||||
type QueryJSON struct {
|
||||
Page string `json:"page"`
|
||||
Publish bool `json:"publish"`
|
||||
}
|
||||
|
||||
var json QueryJSON
|
||||
if c.BindJSON(&json) != nil {
|
||||
c.String(http.StatusBadRequest, "Problem binding keys")
|
||||
return
|
||||
}
|
||||
p := s.Open(json.Page)
|
||||
p.IsPublished = json.Publish
|
||||
p.Save()
|
||||
message := "Published"
|
||||
if !p.IsPublished {
|
||||
message = "Unpublished"
|
||||
}
|
||||
s.sitemapUpToDate = false
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": message})
|
||||
}
|
||||
|
||||
func (s *Site) handleUpload(c *gin.Context) {
|
||||
if !s.Fileuploads {
|
||||
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Uploads are disabled on this server"))
|
||||
return
|
||||
}
|
||||
|
||||
file, info, err := c.Request.FormFile("file")
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, file); err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
newName := "sha256-" + encodeBytesToBase32(h.Sum(nil))
|
||||
|
||||
// Replaces any existing version, but sha256 collisions are rare as anything.
|
||||
outfile, err := os.Create(path.Join(s.PathToData, newName+".upload"))
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
file.Seek(0, io.SeekStart)
|
||||
_, err = io.Copy(outfile, file)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Location", "/uploads/"+newName+"?filename="+url.QueryEscape(info.Filename))
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Site) handleEncrypt(c *gin.Context) {
|
||||
type QueryJSON struct {
|
||||
Page string `json:"page"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
}
|
||||
|
||||
var json QueryJSON
|
||||
if c.BindJSON(&json) != nil {
|
||||
c.String(http.StatusBadRequest, "Problem binding keys")
|
||||
return
|
||||
}
|
||||
p := s.Open(json.Page)
|
||||
if pageIsLocked(p, c) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Locked"})
|
||||
return
|
||||
}
|
||||
q := s.Open(json.Page)
|
||||
var message string
|
||||
if p.IsEncrypted {
|
||||
decrypted, err2 := encrypt.DecryptString(p.Text.GetCurrent(), json.Passphrase)
|
||||
if err2 != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Wrong password"})
|
||||
return
|
||||
}
|
||||
q.Erase()
|
||||
q = s.Open(json.Page)
|
||||
q.Update(decrypted)
|
||||
q.IsEncrypted = false
|
||||
q.IsLocked = p.IsLocked
|
||||
q.IsPrimedForSelfDestruct = p.IsPrimedForSelfDestruct
|
||||
message = "Decrypted"
|
||||
} else {
|
||||
currentText := p.Text.GetCurrent()
|
||||
encrypted, _ := encrypt.EncryptString(currentText, json.Passphrase)
|
||||
q.Erase()
|
||||
q = s.Open(json.Page)
|
||||
q.Update(encrypted)
|
||||
q.IsEncrypted = true
|
||||
q.IsLocked = p.IsLocked
|
||||
q.IsPrimedForSelfDestruct = p.IsPrimedForSelfDestruct
|
||||
message = "Encrypted"
|
||||
}
|
||||
q.Save()
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": message})
|
||||
}
|
||||
|
||||
func (s *Site) deleteListItem(c *gin.Context) {
|
||||
lineNum, err := strconv.Atoi(c.DefaultQuery("lineNum", "None"))
|
||||
page := c.Query("page") // shortcut for c.Request.URL.Query().Get("lastname")
|
||||
if err == nil {
|
||||
p := s.Open(page)
|
||||
|
||||
_, listItems := reorderList(p.Text.GetCurrent())
|
||||
newText := p.Text.GetCurrent()
|
||||
for i, lineString := range listItems {
|
||||
// fmt.Println(i, lineString, lineNum)
|
||||
if i+1 == lineNum {
|
||||
// fmt.Println("MATCHED")
|
||||
if strings.Contains(lineString, "~~") == false {
|
||||
// fmt.Println(p.Text, "("+lineString[2:]+"\n"+")", "~~"+lineString[2:]+"~~"+"\n")
|
||||
newText = strings.Replace(newText+"\n", lineString[2:]+"\n", "~~"+strings.TrimSpace(lineString[2:])+"~~"+"\n", 1)
|
||||
} else {
|
||||
newText = strings.Replace(newText+"\n", lineString[2:]+"\n", lineString[4:len(lineString)-2]+"\n", 1)
|
||||
}
|
||||
p.Update(newText)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"message": "Done.",
|
||||
})
|
||||
} else {
|
||||
c.JSON(200, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Site) handleClearOldListItems(c *gin.Context) {
|
||||
type QueryJSON struct {
|
||||
Page string `json:"page"`
|
||||
}
|
||||
|
||||
var json QueryJSON
|
||||
if c.BindJSON(&json) != nil {
|
||||
c.String(http.StatusBadRequest, "Problem binding keys")
|
||||
return
|
||||
}
|
||||
p := s.Open(json.Page)
|
||||
if p.IsEncrypted {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Encrypted"})
|
||||
return
|
||||
}
|
||||
if pageIsLocked(p, c) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Locked"})
|
||||
return
|
||||
}
|
||||
lines := strings.Split(p.Text.GetCurrent(), "\n")
|
||||
newLines := make([]string, len(lines))
|
||||
newLinesI := 0
|
||||
for _, line := range lines {
|
||||
if strings.Count(line, "~~") != 2 {
|
||||
newLines[newLinesI] = line
|
||||
newLinesI++
|
||||
}
|
||||
}
|
||||
p.Update(strings.Join(newLines[0:newLinesI], "\n"))
|
||||
p.Save()
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Cleared"})
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package server
|
||||
|
||||
import (
|
||||
"html/template"
|
|
@ -0,0 +1,36 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
|
||||
"github.com/jcelliott/lumber"
|
||||
)
|
||||
|
||||
func Migrate(pathToOldData, pathToData string, logger *lumber.ConsoleLogger) error {
|
||||
files, err := ioutil.ReadDir(pathToOldData)
|
||||
if len(files) == 0 {
|
||||
return err
|
||||
}
|
||||
s := Site{PathToData: pathToData, Logger: lumber.NewConsoleLogger(lumber.TRACE)}
|
||||
for _, f := range files {
|
||||
if f.Mode().IsDir() {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Migrating %s", f.Name())
|
||||
p := s.Open(f.Name())
|
||||
bData, err := ioutil.ReadFile(path.Join(pathToOldData, f.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.Update(string(bData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = p.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/schollz/versionedtext"
|
||||
)
|
||||
|
||||
// Page is the basic struct
|
||||
type Page struct {
|
||||
Site *Site
|
||||
|
||||
Name string
|
||||
Text versionedtext.VersionedText
|
||||
Meta string
|
||||
RenderedPage string
|
||||
IsLocked bool
|
||||
PassphraseToUnlock string
|
||||
IsEncrypted bool
|
||||
IsPrimedForSelfDestruct bool
|
||||
IsPublished bool
|
||||
UnlockedFor string
|
||||
}
|
||||
|
||||
func (p Page) LastEditTime() time.Time {
|
||||
return time.Unix(p.LastEditUnixTime(), 0)
|
||||
}
|
||||
|
||||
func (p Page) LastEditUnixTime() int64 {
|
||||
return p.Text.LastEditTime() / 1000000000
|
||||
}
|
||||
|
||||
func (s *Site) Open(name string) (p *Page) {
|
||||
p = new(Page)
|
||||
p.Site = s
|
||||
p.Name = name
|
||||
p.Text = versionedtext.NewVersionedText("")
|
||||
p.Render()
|
||||
bJSON, err := ioutil.ReadFile(path.Join(s.PathToData, encodeToBase32(strings.ToLower(name))+".json"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(bJSON, &p)
|
||||
if err != nil {
|
||||
p = new(Page)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
type DirectoryEntry struct {
|
||||
Path string
|
||||
Length int
|
||||
Numchanges int
|
||||
LastEdited time.Time
|
||||
}
|
||||
|
||||
func (d DirectoryEntry) LastEditTime() string {
|
||||
return d.LastEdited.Format("Mon Jan 2 15:04:05 MST 2006")
|
||||
}
|
||||
|
||||
func (d DirectoryEntry) Name() string {
|
||||
return d.Path
|
||||
}
|
||||
|
||||
func (d DirectoryEntry) Size() int64 {
|
||||
return int64(d.Length)
|
||||
}
|
||||
|
||||
func (d DirectoryEntry) Mode() os.FileMode {
|
||||
return os.ModePerm
|
||||
}
|
||||
|
||||
func (d DirectoryEntry) ModTime() time.Time {
|
||||
return d.LastEdited
|
||||
}
|
||||
|
||||
func (d DirectoryEntry) IsDir() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (d DirectoryEntry) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Site) DirectoryList() []os.FileInfo {
|
||||
files, _ := ioutil.ReadDir(s.PathToData)
|
||||
entries := make([]os.FileInfo, len(files))
|
||||
for i, f := range files {
|
||||
name := DecodeFileName(f.Name())
|
||||
p := s.Open(name)
|
||||
entries[i] = DirectoryEntry{
|
||||
Path: name,
|
||||
Length: len(p.Text.GetCurrent()),
|
||||
Numchanges: p.Text.NumEdits(),
|
||||
LastEdited: time.Unix(p.Text.LastEditTime()/1000000000, 0),
|
||||
}
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool { return entries[i].ModTime().After(entries[j].ModTime()) })
|
||||
return entries
|
||||
}
|
||||
|
||||
type UploadEntry struct {
|
||||
os.FileInfo
|
||||
}
|
||||
|
||||
func (s *Site) UploadList() ([]os.FileInfo, error) {
|
||||
paths, err := filepath.Glob(path.Join(s.PathToData, "sha256*"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]os.FileInfo, len(paths))
|
||||
for i := range paths {
|
||||
result[i], err = os.Stat(paths[i])
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func DecodeFileName(s string) string {
|
||||
s2, _ := decodeFromBase32(strings.Split(s, ".")[0])
|
||||
return s2
|
||||
}
|
||||
|
||||
// Update cleans the text and updates the versioned text
|
||||
// and generates a new render
|
||||
func (p *Page) Update(newText string) error {
|
||||
// Trim space from end
|
||||
newText = strings.TrimRight(newText, "\n\t ")
|
||||
|
||||
// Update the versioned text
|
||||
p.Text.Update(newText)
|
||||
|
||||
// Render the new page
|
||||
p.Render()
|
||||
|
||||
return p.Save()
|
||||
}
|
||||
|
||||
var rBracketPage = regexp.MustCompile(`\[\[(.*?)\]\]`)
|
||||
|
||||
func (p *Page) Render() {
|
||||
if p.IsEncrypted {
|
||||
p.RenderedPage = "<code>" + p.Text.GetCurrent() + "</code>"
|
||||
return
|
||||
}
|
||||
|
||||
// Convert [[page]] to [page](/page/view)
|
||||
currentText := p.Text.GetCurrent()
|
||||
for _, s := range rBracketPage.FindAllString(currentText, -1) {
|
||||
currentText = strings.Replace(currentText, s, "["+s[2:len(s)-2]+"](/"+s[2:len(s)-2]+"/view)", 1)
|
||||
}
|
||||
p.Text.Update(currentText)
|
||||
p.RenderedPage = MarkdownToHtml(p.Text.GetCurrent())
|
||||
}
|
||||
|
||||
func (p *Page) Save() error {
|
||||
p.Site.saveMut.Lock()
|
||||
defer p.Site.saveMut.Unlock()
|
||||
bJSON, err := json.MarshalIndent(p, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(path.Join(p.Site.PathToData, encodeToBase32(strings.ToLower(p.Name))+".json"), bJSON, 0644)
|
||||
}
|
||||
|
||||
func (p *Page) ChildPageNames() []string {
|
||||
prefix := strings.ToLower(p.Name + ": ")
|
||||
files, err := filepath.Glob(path.Join(p.Site.PathToData, "*"))
|
||||
if err != nil {
|
||||
panic("Filepath pattern cannot be malformed")
|
||||
}
|
||||
|
||||
result := []string{}
|
||||
for i := range files {
|
||||
basename := filepath.Base(files[i])
|
||||
if strings.HasSuffix(basename, ".json") {
|
||||
cname, err := decodeFromBase32(basename[:len(basename)-len(".json")])
|
||||
if err == nil && strings.HasPrefix(strings.ToLower(cname), prefix) {
|
||||
result = append(result, cname)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *Page) IsNew() bool {
|
||||
return !exists(path.Join(p.Site.PathToData, encodeToBase32(strings.ToLower(p.Name))+".json"))
|
||||
}
|
||||
|
||||
func (p *Page) Erase() error {
|
||||
p.Site.Logger.Trace("Erasing " + p.Name)
|
||||
return os.Remove(path.Join(p.Site.PathToData, encodeToBase32(strings.ToLower(p.Name))+".json"))
|
||||
}
|
||||
|
||||
func (p *Page) Published() bool {
|
||||
return p.IsPublished
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListFiles(t *testing.T) {
|
||||
pathToData = "testdata"
|
||||
os.MkdirAll(pathToData, 0755)
|
||||
defer os.RemoveAll(pathToData)
|
||||
s := Site{PathToData: pathToData}
|
||||
p := s.Open("testpage")
|
||||
p.Update("Some data")
|
||||
p = s.Open("testpage2")
|
||||
p.Update("A different bunch of data")
|
||||
p = s.Open("testpage3")
|
||||
p.Update("Not much else")
|
||||
n := s.DirectoryList()
|
||||
if len(n) != 3 {
|
||||
t.Error("Expected three directory entries")
|
||||
t.FailNow()
|
||||
}
|
||||
if n[0].Name() != "testpage" {
|
||||
t.Error("Expected testpage to be first")
|
||||
}
|
||||
if n[1].Name() != "testpage2" {
|
||||
t.Error("Expected testpage2 to be second")
|
||||
}
|
||||
if n[2].Name() != "testpage3" {
|
||||
t.Error("Expected testpage3 to be last")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneral(t *testing.T) {
|
||||
pathToData = "testdata"
|
||||
os.MkdirAll(pathToData, 0755)
|
||||
defer os.RemoveAll(pathToData)
|
||||
s := Site{PathToData: pathToData}
|
||||
p := s.Open("testpage")
|
||||
err := p.Update("**bold**")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if strings.TrimSpace(p.RenderedPage) != "<p><strong>bold</strong></p>" {
|
||||
t.Errorf("Did not render: '%s'", p.RenderedPage)
|
||||
}
|
||||
err = p.Update("**bold** and *italic*")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
p.Save()
|
||||
|
||||
p2 := s.Open("testpage")
|
||||
if strings.TrimSpace(p2.RenderedPage) != "<p><strong>bold</strong> and <em>italic</em></p>" {
|
||||
t.Errorf("Did not render: '%s'", p2.RenderedPage)
|
||||
}
|
||||
|
||||
p3 := s.Open("testpage: childpage")
|
||||
err = p3.Update("**child content**")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
children := p.ChildPageNames()
|
||||
if len(children) != 1 {
|
||||
t.Errorf("Expected 1 child page to be found, got %d", len(children))
|
||||
return
|
||||
}
|
||||
if children[0] != "testpage: childpage" {
|
||||
t.Errorf("Expected child page %s to be found (got %s)", "testpage: childpage", children[0])
|
||||
}
|
||||
}
|
|
@ -1,18 +1,18 @@
|
|||
package main
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jcelliott/lumber"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/russross/blackfriday"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
"github.com/shurcooL/github_flavored_markdown"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
@ -20,8 +20,7 @@ import (
|
|||
var animals []string
|
||||
var adjectives []string
|
||||
var aboutPageText string
|
||||
|
||||
var log *lumber.ConsoleLogger
|
||||
var allowInsecureHtml bool
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().Unix())
|
||||
|
@ -29,11 +28,6 @@ func init() {
|
|||
animals = strings.Split(string(animalsText), ",")
|
||||
adjectivesText, _ := Asset("static/text/adjectives")
|
||||
adjectives = strings.Split(string(adjectivesText), "\n")
|
||||
log = lumber.NewConsoleLogger(lumber.TRACE)
|
||||
}
|
||||
|
||||
func turnOffDebugger() {
|
||||
log = lumber.NewConsoleLogger(lumber.WARN)
|
||||
}
|
||||
|
||||
func randomAnimal() string {
|
||||
|
@ -85,13 +79,29 @@ func contentType(filename string) string {
|
|||
return "image/png"
|
||||
case strings.Contains(filename, ".js"):
|
||||
return "application/javascript"
|
||||
case strings.Contains(filename, ".xml"):
|
||||
return "application/xml"
|
||||
}
|
||||
return "text/html"
|
||||
}
|
||||
|
||||
func timeTrack(start time.Time, name string) {
|
||||
elapsed := time.Since(start)
|
||||
log.Debug("%s took %s", name, elapsed)
|
||||
func (s *Site) sniffContentType(name string) (string, error) {
|
||||
file, err := os.Open(path.Join(s.PathToData, name))
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Only the first 512 bytes are used to sniff the content type.
|
||||
buffer := make([]byte, 512)
|
||||
_, err = file.Read(buffer)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Always returns a valid content-type and "application/octet-stream" if no others seemed to match.
|
||||
return http.DetectContentType(buffer), nil
|
||||
}
|
||||
|
||||
var src = rand.NewSource(time.Now().UnixNano())
|
||||
|
@ -122,24 +132,6 @@ func RandStringBytesMaskImprSrc(n int) string {
|
|||
return string(b)
|
||||
}
|
||||
|
||||
// GetLocalIP returns the local ip address
|
||||
func GetLocalIP() string {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
bestIP := ""
|
||||
for _, address := range addrs {
|
||||
// check the address type and if it is not a loopback the display it
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
return ipnet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestIP
|
||||
}
|
||||
|
||||
// HashPassword generates a bcrypt hash of the password using work factor 14.
|
||||
// https://github.com/gtank/cryptopasta/blob/master/hash.go
|
||||
func HashPassword(password string) string {
|
||||
|
@ -161,19 +153,18 @@ func CheckPasswordHash(password, hashedString string) error {
|
|||
// exists returns whether the given file or directory exists or not
|
||||
func exists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func MarkdownToHtml(s string) string {
|
||||
unsafe := blackfriday.MarkdownCommon([]byte(s))
|
||||
unsafe := blackfriday.Run([]byte(s))
|
||||
if allowInsecureHtml {
|
||||
return string(unsafe)
|
||||
}
|
||||
|
||||
pClean := bluemonday.UGCPolicy()
|
||||
pClean.AllowElements("img")
|
||||
pClean.AllowElements("center")
|
||||
pClean.AllowAttrs("alt").OnElements("img")
|
||||
pClean.AllowAttrs("src").OnElements("img")
|
||||
pClean.AllowAttrs("class").OnElements("a")
|
||||
|
@ -187,8 +178,13 @@ func MarkdownToHtml(s string) string {
|
|||
func GithubMarkdownToHTML(s string) string {
|
||||
return string(github_flavored_markdown.Markdown([]byte(s)))
|
||||
}
|
||||
|
||||
func encodeToBase32(s string) string {
|
||||
return base32.StdEncoding.EncodeToString([]byte(s))
|
||||
return encodeBytesToBase32([]byte(s))
|
||||
}
|
||||
|
||||
func encodeBytesToBase32(s []byte) string {
|
||||
return base32.StdEncoding.EncodeToString(s)
|
||||
}
|
||||
|
||||
func decodeFromBase32(s string) (s2 string, err error) {
|
|
@ -1,34 +1,34 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkAlliterativeAnimal(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
randomAlliterateCombo()
|
||||
}
|
||||
}
|
||||
|
||||
func TestReverseList(t *testing.T) {
|
||||
s := []int64{1, 10, 2, 20}
|
||||
if reverseSliceInt64(s)[0] != 20 {
|
||||
t.Errorf("Could not reverse: %v", s)
|
||||
}
|
||||
s2 := []string{"a", "b", "d", "c"}
|
||||
if reverseSliceString(s2)[0] != "c" {
|
||||
t.Errorf("Could not reverse: %v", s2)
|
||||
}
|
||||
}
|
||||
func TestHashing(t *testing.T) {
|
||||
p := HashPassword("1234")
|
||||
log.Debug(p)
|
||||
err := CheckPasswordHash("1234", p)
|
||||
if err != nil {
|
||||
t.Errorf("Should be correct password")
|
||||
}
|
||||
err = CheckPasswordHash("1234lkjklj", p)
|
||||
if err == nil {
|
||||
t.Errorf("Should NOT be correct password")
|
||||
}
|
||||
}
|
||||
package server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkAlliterativeAnimal(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
randomAlliterateCombo()
|
||||
}
|
||||
}
|
||||
|
||||
func TestReverseList(t *testing.T) {
|
||||
s := []int64{1, 10, 2, 20}
|
||||
if reverseSliceInt64(s)[0] != 20 {
|
||||
t.Errorf("Could not reverse: %v", s)
|
||||
}
|
||||
s2 := []string{"a", "b", "d", "c"}
|
||||
if reverseSliceString(s2)[0] != "c" {
|
||||
t.Errorf("Could not reverse: %v", s2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashing(t *testing.T) {
|
||||
p := HashPassword("1234")
|
||||
err := CheckPasswordHash("1234", p)
|
||||
if err != nil {
|
||||
t.Errorf("Should be correct password")
|
||||
}
|
||||
err = CheckPasswordHash("1234lkjklj", p)
|
||||
if err == nil {
|
||||
t.Errorf("Should NOT be correct password")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
body.ListPage span {
|
||||
cursor: pointer;
|
||||
}
|
||||
body {
|
||||
background: #fff;
|
||||
}
|
||||
.success {
|
||||
color: #5cb85c;
|
||||
font-weight: bold;
|
||||
}
|
||||
.failure {
|
||||
color: #d9534f;
|
||||
font-weight: bold;
|
||||
}
|
||||
.pure-menu a {
|
||||
color: #777;
|
||||
}
|
||||
.deleting {
|
||||
opacity: 0.5;
|
||||
}
|
||||
#wrap {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
}
|
||||
#pad {
|
||||
height:100%;
|
||||
}
|
||||
|
||||
body.EditPage {
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
body#pad textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
border: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
resize: none;
|
||||
font-size: 1.0em;
|
||||
font-family: 'Open Sans','Segoe UI',Tahoma,Arial,sans-serif;
|
||||
}
|
||||
|
||||
body#pad.HasDotInName textarea {
|
||||
font-family: "Lucida Console", Monaco, monospace;
|
||||
}
|
||||
|
||||
.markdown-body ul, .markdown-body ol {
|
||||
padding-left: 0em;
|
||||
}
|
||||
|
||||
@media (min-width: 5em) {
|
||||
div#menu, div#rendered, .ChildPageNames, body#pad textarea {
|
||||
padding-left: 2%;
|
||||
padding-right: 2%;
|
||||
}
|
||||
.pure-menu .pure-menu-horizontal {
|
||||
max-width: 300px;
|
||||
}
|
||||
.pure-menu-disabled, .pure-menu-heading, .pure-menu-link {
|
||||
padding-left:1.2em;
|
||||
padding-right:em;
|
||||
}
|
||||
.ChildPageNames ul {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
}
|
||||
@media (min-width: 50em) {
|
||||
div#menu, div#rendered, .ChildPageNames, body#pad textarea {
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
.pure-menu-disabled, .pure-menu-heading, .pure-menu-link {
|
||||
padding: .5em 1em;
|
||||
}
|
||||
.ChildPageNames ul {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@media (min-width: 70em) {
|
||||
div#menu, div#rendered, .ChildPageNames, body#pad textarea {
|
||||
padding-left: 15%;
|
||||
padding-right: 15%;
|
||||
}
|
||||
.ChildPageNames ul {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 100em) {
|
||||
div#menu, div#rendered, .ChildPageNames, body#pad textarea {
|
||||
padding-left: 20%;
|
||||
padding-right: 20%;
|
||||
}
|
||||
.ChildPageNames ul {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.ChildPageNames ul {
|
||||
margin: 0.3em 0 0 1.6em;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.ChildPageNames li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ChildPageNames a {
|
||||
color: #0645ad;
|
||||
background: none;
|
||||
}
|
|
@ -0,0 +1,388 @@
|
|||
/*
|
||||
* The MIT License
|
||||
* Copyright (c) 2012 Matias Meno <m@tias.me>
|
||||
*/
|
||||
@-webkit-keyframes passing-through {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px); }
|
||||
30%, 70% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px); }
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(-40px);
|
||||
-moz-transform: translateY(-40px);
|
||||
-ms-transform: translateY(-40px);
|
||||
-o-transform: translateY(-40px);
|
||||
transform: translateY(-40px); } }
|
||||
@-moz-keyframes passing-through {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px); }
|
||||
30%, 70% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px); }
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(-40px);
|
||||
-moz-transform: translateY(-40px);
|
||||
-ms-transform: translateY(-40px);
|
||||
-o-transform: translateY(-40px);
|
||||
transform: translateY(-40px); } }
|
||||
@keyframes passing-through {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px); }
|
||||
30%, 70% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px); }
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(-40px);
|
||||
-moz-transform: translateY(-40px);
|
||||
-ms-transform: translateY(-40px);
|
||||
-o-transform: translateY(-40px);
|
||||
transform: translateY(-40px); } }
|
||||
@-webkit-keyframes slide-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px); }
|
||||
30% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px); } }
|
||||
@-moz-keyframes slide-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px); }
|
||||
30% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px); } }
|
||||
@keyframes slide-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px); }
|
||||
30% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px); } }
|
||||
@-webkit-keyframes pulse {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1); }
|
||||
10% {
|
||||
-webkit-transform: scale(1.1);
|
||||
-moz-transform: scale(1.1);
|
||||
-ms-transform: scale(1.1);
|
||||
-o-transform: scale(1.1);
|
||||
transform: scale(1.1); }
|
||||
20% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1); } }
|
||||
@-moz-keyframes pulse {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1); }
|
||||
10% {
|
||||
-webkit-transform: scale(1.1);
|
||||
-moz-transform: scale(1.1);
|
||||
-ms-transform: scale(1.1);
|
||||
-o-transform: scale(1.1);
|
||||
transform: scale(1.1); }
|
||||
20% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1); } }
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1); }
|
||||
10% {
|
||||
-webkit-transform: scale(1.1);
|
||||
-moz-transform: scale(1.1);
|
||||
-ms-transform: scale(1.1);
|
||||
-o-transform: scale(1.1);
|
||||
transform: scale(1.1); }
|
||||
20% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1); } }
|
||||
.dropzone, .dropzone * {
|
||||
box-sizing: border-box; }
|
||||
|
||||
.dropzone {
|
||||
min-height: 150px;
|
||||
border: 2px solid rgba(0, 0, 0, 0.3);
|
||||
background: white;
|
||||
padding: 20px 20px; }
|
||||
.dropzone.dz-clickable {
|
||||
cursor: pointer; }
|
||||
.dropzone.dz-clickable * {
|
||||
cursor: default; }
|
||||
.dropzone.dz-clickable .dz-message, .dropzone.dz-clickable .dz-message * {
|
||||
cursor: pointer; }
|
||||
.dropzone.dz-started .dz-message {
|
||||
display: none; }
|
||||
.dropzone.dz-drag-hover {
|
||||
border-style: solid; }
|
||||
.dropzone.dz-drag-hover .dz-message {
|
||||
opacity: 0.5; }
|
||||
.dropzone .dz-message {
|
||||
text-align: center;
|
||||
margin: 2em 0; }
|
||||
.dropzone .dz-preview {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 16px;
|
||||
min-height: 100px; }
|
||||
.dropzone .dz-preview:hover {
|
||||
z-index: 1000; }
|
||||
.dropzone .dz-preview:hover .dz-details {
|
||||
opacity: 1; }
|
||||
.dropzone .dz-preview.dz-file-preview .dz-image {
|
||||
border-radius: 20px;
|
||||
background: #999;
|
||||
background: linear-gradient(to bottom, #eee, #ddd); }
|
||||
.dropzone .dz-preview.dz-file-preview .dz-details {
|
||||
opacity: 1; }
|
||||
.dropzone .dz-preview.dz-image-preview {
|
||||
background: white; }
|
||||
.dropzone .dz-preview.dz-image-preview .dz-details {
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
-ms-transition: opacity 0.2s linear;
|
||||
-o-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear; }
|
||||
.dropzone .dz-preview .dz-remove {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
border: none; }
|
||||
.dropzone .dz-preview .dz-remove:hover {
|
||||
text-decoration: underline; }
|
||||
.dropzone .dz-preview:hover .dz-details {
|
||||
opacity: 1; }
|
||||
.dropzone .dz-preview .dz-details {
|
||||
z-index: 20;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
font-size: 13px;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 2em 1em;
|
||||
text-align: center;
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
line-height: 150%; }
|
||||
.dropzone .dz-preview .dz-details .dz-size {
|
||||
margin-bottom: 1em;
|
||||
font-size: 16px; }
|
||||
.dropzone .dz-preview .dz-details .dz-filename {
|
||||
white-space: nowrap; }
|
||||
.dropzone .dz-preview .dz-details .dz-filename:hover span {
|
||||
border: 1px solid rgba(200, 200, 200, 0.8);
|
||||
background-color: rgba(255, 255, 255, 0.8); }
|
||||
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; }
|
||||
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
|
||||
border: 1px solid transparent; }
|
||||
.dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span {
|
||||
background-color: rgba(255, 255, 255, 0.4);
|
||||
padding: 0 0.4em;
|
||||
border-radius: 3px; }
|
||||
.dropzone .dz-preview:hover .dz-image img {
|
||||
-webkit-transform: scale(1.05, 1.05);
|
||||
-moz-transform: scale(1.05, 1.05);
|
||||
-ms-transform: scale(1.05, 1.05);
|
||||
-o-transform: scale(1.05, 1.05);
|
||||
transform: scale(1.05, 1.05);
|
||||
-webkit-filter: blur(8px);
|
||||
filter: blur(8px); }
|
||||
.dropzone .dz-preview .dz-image {
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
position: relative;
|
||||
display: block;
|
||||
z-index: 10; }
|
||||
.dropzone .dz-preview .dz-image img {
|
||||
display: block; }
|
||||
.dropzone .dz-preview.dz-success .dz-success-mark {
|
||||
-webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); }
|
||||
.dropzone .dz-preview.dz-error .dz-error-mark {
|
||||
opacity: 1;
|
||||
-webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); }
|
||||
.dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
z-index: 500;
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -27px;
|
||||
margin-top: -27px; }
|
||||
.dropzone .dz-preview .dz-success-mark svg, .dropzone .dz-preview .dz-error-mark svg {
|
||||
display: block;
|
||||
width: 54px;
|
||||
height: 54px; }
|
||||
.dropzone .dz-preview.dz-processing .dz-progress {
|
||||
opacity: 1;
|
||||
-webkit-transition: all 0.2s linear;
|
||||
-moz-transition: all 0.2s linear;
|
||||
-ms-transition: all 0.2s linear;
|
||||
-o-transition: all 0.2s linear;
|
||||
transition: all 0.2s linear; }
|
||||
.dropzone .dz-preview.dz-complete .dz-progress {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.4s ease-in;
|
||||
-moz-transition: opacity 0.4s ease-in;
|
||||
-ms-transition: opacity 0.4s ease-in;
|
||||
-o-transition: opacity 0.4s ease-in;
|
||||
transition: opacity 0.4s ease-in; }
|
||||
.dropzone .dz-preview:not(.dz-processing) .dz-progress {
|
||||
-webkit-animation: pulse 6s ease infinite;
|
||||
-moz-animation: pulse 6s ease infinite;
|
||||
-ms-animation: pulse 6s ease infinite;
|
||||
-o-animation: pulse 6s ease infinite;
|
||||
animation: pulse 6s ease infinite; }
|
||||
.dropzone .dz-preview .dz-progress {
|
||||
opacity: 1;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
height: 16px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
width: 80px;
|
||||
margin-left: -40px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
-webkit-transform: scale(1);
|
||||
border-radius: 8px;
|
||||
overflow: hidden; }
|
||||
.dropzone .dz-preview .dz-progress .dz-upload {
|
||||
background: #333;
|
||||
background: linear-gradient(to bottom, #666, #444);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
-webkit-transition: width 300ms ease-in-out;
|
||||
-moz-transition: width 300ms ease-in-out;
|
||||
-ms-transition: width 300ms ease-in-out;
|
||||
-o-transition: width 300ms ease-in-out;
|
||||
transition: width 300ms ease-in-out; }
|
||||
.dropzone .dz-preview.dz-error .dz-error-message {
|
||||
display: block; }
|
||||
.dropzone .dz-preview.dz-error:hover .dz-error-message {
|
||||
opacity: 1;
|
||||
pointer-events: auto; }
|
||||
.dropzone .dz-preview .dz-error-message {
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
display: block;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.3s ease;
|
||||
-moz-transition: opacity 0.3s ease;
|
||||
-ms-transition: opacity 0.3s ease;
|
||||
-o-transition: opacity 0.3s ease;
|
||||
transition: opacity 0.3s ease;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
top: 130px;
|
||||
left: -10px;
|
||||
width: 140px;
|
||||
background: #be2626;
|
||||
background: linear-gradient(to bottom, #be2626, #a92222);
|
||||
padding: 0.5em 1.2em;
|
||||
color: white; }
|
||||
.dropzone .dz-preview .dz-error-message:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 64px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-bottom: 6px solid #be2626; }
|
|
@ -0,0 +1,388 @@
|
|||
"use strict";
|
||||
var oulipo = false;
|
||||
|
||||
$(window).load(function() {
|
||||
// Returns a function, that, as long as it continues to be invoked, will not
|
||||
// be triggered. The function will be called after it stops being called for
|
||||
// N milliseconds. If `immediate` is passed, trigger the function on the
|
||||
// leading edge, instead of the trailing.
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
$('#saveEditButton').removeClass()
|
||||
$('#saveEditButton').text("Editing");
|
||||
var context = this,
|
||||
args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
};
|
||||
|
||||
// This will apply the debounce effect on the keyup event
|
||||
// And it only fires 500ms or half a second after the user stopped typing
|
||||
var prevText = $('#userInput').val();
|
||||
console.log("debounce: " + window.cowyo.debounceMS)
|
||||
$('#userInput').on('keyup', debounce(function() {
|
||||
if (prevText == $('#userInput').val()) {
|
||||
return // no changes
|
||||
}
|
||||
prevText = $('#userInput').val();
|
||||
|
||||
if (oulipo) {
|
||||
$('#userInput').val($('#userInput').val().replace(/e/g,""));
|
||||
}
|
||||
$('#saveEditButton').removeClass()
|
||||
$('#saveEditButton').text("Saving")
|
||||
upload();
|
||||
}, window.cowyo.debounceMS));
|
||||
|
||||
var latestUpload = null, needAnother = false;
|
||||
function upload() {
|
||||
// Prevent concurrent uploads
|
||||
if (latestUpload != null) {
|
||||
needAnother = true;
|
||||
return
|
||||
}
|
||||
latestUpload = $.ajax({
|
||||
type: 'POST',
|
||||
url: '/update',
|
||||
data: JSON.stringify({
|
||||
new_text: $('#userInput').val(),
|
||||
page: window.cowyo.pageName,
|
||||
fetched_at: window.lastFetch,
|
||||
}),
|
||||
success: function(data) {
|
||||
latestUpload = null;
|
||||
|
||||
$('#saveEditButton').removeClass()
|
||||
if (data.success == true) {
|
||||
$('#saveEditButton').addClass("success");
|
||||
window.lastFetch = data.unix_time;
|
||||
|
||||
if (needAnother) {
|
||||
upload();
|
||||
};
|
||||
} else {
|
||||
$('#saveEditButton').addClass("failure");
|
||||
}
|
||||
$('#saveEditButton').text(data.message);
|
||||
needAnother = false;
|
||||
},
|
||||
error: function(xhr, error) {
|
||||
latestUpload = null;
|
||||
needAnother = false;
|
||||
$('#saveEditButton').removeClass()
|
||||
$('#saveEditButton').addClass("failure");
|
||||
$('#saveEditButton').text(error);
|
||||
},
|
||||
contentType: "application/json",
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
|
||||
function primeForSelfDestruct() {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/prime',
|
||||
data: JSON.stringify({
|
||||
page: window.cowyo.pageName,
|
||||
}),
|
||||
success: function(data) {
|
||||
$('#saveEditButton').removeClass()
|
||||
if (data.success == true) {
|
||||
$('#saveEditButton').addClass("success");
|
||||
} else {
|
||||
$('#saveEditButton').addClass("failure");
|
||||
}
|
||||
$('#saveEditButton').text(data.message);
|
||||
},
|
||||
error: function(xhr, error) {
|
||||
$('#saveEditButton').removeClass()
|
||||
$('#saveEditButton').addClass("failure");
|
||||
$('#saveEditButton').text(error);
|
||||
},
|
||||
contentType: "application/json",
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
|
||||
function lockPage(passphrase) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/lock',
|
||||
data: JSON.stringify({
|
||||
page: window.cowyo.pageName,
|
||||
passphrase: passphrase
|
||||
}),
|
||||
success: function(data) {
|
||||
$('#saveEditButton').removeClass()
|
||||
if (data.success == true) {
|
||||
$('#saveEditButton').addClass("success");
|
||||
} else {
|
||||
$('#saveEditButton').addClass("failure");
|
||||
}
|
||||
$('#saveEditButton').text(data.message);
|
||||
if (data.success == true && $('#lockPage').text() == "Lock") {
|
||||
window.location = "/" + window.cowyo.pageName + "/view";
|
||||
}
|
||||
if (data.success == true && $('#lockPage').text() == "Unlock") {
|
||||
window.location = "/" + window.cowyo.pageName + "/edit";
|
||||
}
|
||||
},
|
||||
error: function(xhr, error) {
|
||||
$('#saveEditButton').removeClass()
|
||||
$('#saveEditButton').addClass("failure");
|
||||
$('#saveEditButton').text(error);
|
||||
},
|
||||
contentType: "application/json",
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
|
||||
function publishPage() {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/publish',
|
||||
data: JSON.stringify({
|
||||
page: window.cowyo.pageName,
|
||||
publish: $('#publishPage').text() == "Publish"
|
||||
}),
|
||||
success: function(data) {
|
||||
$('#saveEditButton').removeClass()
|
||||
if (data.success == true) {
|
||||
$('#saveEditButton').addClass("success");
|
||||
} else {
|
||||
$('#saveEditButton').addClass("failure");
|
||||
}
|
||||
$('#saveEditButton').text(data.message);
|
||||
if (data.message == "Unpublished") {
|
||||
$('#publishPage').text("Publish");
|
||||
} else {
|
||||
$('#publishPage').text("Unpublish");
|
||||
}
|
||||
},
|
||||
error: function(xhr, error) {
|
||||
$('#saveEditButton').removeClass()
|
||||
$('#saveEditButton').addClass("failure");
|
||||
$('#saveEditButton').text(error);
|
||||
},
|
||||
contentType: "application/json",
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
|
||||
function encryptPage(passphrase) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/encrypt',
|
||||
data: JSON.stringify({
|
||||
page: window.cowyo.pageName,
|
||||
passphrase: passphrase
|
||||
}),
|
||||
success: function(data) {
|
||||
$('#saveEditButton').removeClass()
|
||||
if (data.success == true) {
|
||||
$('#saveEditButton').addClass("success");
|
||||
} else {
|
||||
$('#saveEditButton').addClass("failure");
|
||||
}
|
||||
$('#saveEditButton').text(data.message);
|
||||
|
||||
if (data.success == true && $('#encryptPage').text() == "Encrypt") {
|
||||
window.location = "/" + window.cowyo.pageName + "/view";
|
||||
}
|
||||
if (data.success == true && $('#encryptPage').text() == "Decrypt") {
|
||||
window.location = "/" + window.cowyo.pageName + "/edit";
|
||||
}
|
||||
},
|
||||
error: function(xhr, error) {
|
||||
$('#saveEditButton').removeClass()
|
||||
$('#saveEditButton').addClass("failure");
|
||||
$('#saveEditButton').text(error);
|
||||
},
|
||||
contentType: "application/json",
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
|
||||
function clearOld() {
|
||||
$.ajax({
|
||||
type: 'DELETE',
|
||||
url: '/oldlist',
|
||||
data: JSON.stringify({
|
||||
page: window.cowyo.pageName
|
||||
}),
|
||||
success: function(data) {
|
||||
$('#saveEditButton').removeClass()
|
||||
if (data.success == true) {
|
||||
$('#saveEditButton').addClass("success");
|
||||
} else {
|
||||
$('#saveEditButton').addClass("failure");
|
||||
}
|
||||
$('#saveEditButton').text(data.message);
|
||||
if (data.success == true) {
|
||||
window.location = "/" + window.cowyo.pageName + "/list";
|
||||
}
|
||||
},
|
||||
error: function(xhr, error) {
|
||||
$('#saveEditButton').removeClass();
|
||||
$('#saveEditButton').addClass("failure");
|
||||
$('#saveEditButton').text(error);
|
||||
},
|
||||
contentType: "application/json",
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
|
||||
$("#encryptPage").click(function(e) {
|
||||
e.preventDefault();
|
||||
var passphrase = prompt("Please enter a passphrase. Note: Encrypting will remove all previous history.", "");
|
||||
if (passphrase != null) {
|
||||
encryptPage(passphrase);
|
||||
}
|
||||
});
|
||||
|
||||
$("#erasePage").click(function(e) {
|
||||
e.preventDefault();
|
||||
var r = confirm("Are you sure you want to erase?");
|
||||
if (r == true) {
|
||||
window.location = "/" + window.cowyo.pageName + "/erase";
|
||||
} else {
|
||||
x = "You pressed Cancel!";
|
||||
}
|
||||
});
|
||||
|
||||
$("#selfDestructPage").click(function(e) {
|
||||
e.preventDefault();
|
||||
var r = confirm("This will erase the page the next time it is opened, are you sure you want to do that?");
|
||||
if (r == true) {
|
||||
primeForSelfDestruct();
|
||||
} else {
|
||||
x = "You pressed Cancel!";
|
||||
}
|
||||
});
|
||||
|
||||
$("#lockPage").click(function(e) {
|
||||
e.preventDefault();
|
||||
var passphrase = prompt("Please enter a passphrase to lock", "");
|
||||
if (passphrase != null) {
|
||||
if ($('#lockPage').text() == "Lock") {
|
||||
$('#saveEditButton').removeClass();
|
||||
$("#saveEditButton").text("Locking");
|
||||
} else {
|
||||
$('#saveEditButton').removeClass();
|
||||
$("#saveEditButton").text("Unlocking");
|
||||
}
|
||||
lockPage(passphrase);
|
||||
// POST encrypt page
|
||||
// reload page
|
||||
}
|
||||
});
|
||||
|
||||
$("#publishPage").click(function(e) {
|
||||
e.preventDefault();
|
||||
var message = " This will add your page to the sitemap.xml so it will be indexed by search engines.";
|
||||
if ($('#publishPage').text() == "Unpublish") {
|
||||
message = "";
|
||||
}
|
||||
var confirmed = confirm("Are you sure?" + message);
|
||||
if (confirmed == true) {
|
||||
if ($('#publishPage').text() == "Unpublish") {
|
||||
$('#saveEditButton').removeClass();
|
||||
$("#saveEditButton").text("Unpublishing");
|
||||
} else {
|
||||
$('#saveEditButton').removeClass();
|
||||
$("#saveEditButton").text("Publishing");
|
||||
}
|
||||
publishPage();
|
||||
}
|
||||
});
|
||||
|
||||
$("#clearOld").click(function(e) {
|
||||
e.preventDefault();
|
||||
var r = confirm("This will erase all cleared list items, are you sure you want to do that? (Versions will stay in history).");
|
||||
if (r == true) {
|
||||
clearOld()
|
||||
} else {
|
||||
x = "You pressed Cancel!";
|
||||
}
|
||||
});
|
||||
|
||||
$("textarea").keydown(function(e) {
|
||||
if(e.keyCode === 9) { // tab was pressed
|
||||
// get caret position/selection
|
||||
var start = this.selectionStart;
|
||||
var end = this.selectionEnd;
|
||||
|
||||
var $this = $(this);
|
||||
var value = $this.val();
|
||||
|
||||
// set textarea value to: text before caret + tab + text after caret
|
||||
$this.val(value.substring(0, start)
|
||||
+ "\t"
|
||||
+ value.substring(end));
|
||||
|
||||
// put caret at right position again (add one for the tab)
|
||||
this.selectionStart = this.selectionEnd = start + 1;
|
||||
|
||||
// prevent the focus lose
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
$('.deletable').click(function(event) {
|
||||
event.preventDefault();
|
||||
var lineNum = $(this).attr('id');
|
||||
|
||||
$.ajax({
|
||||
url: "/listitem" + '?' + $.param({
|
||||
"lineNum": lineNum,
|
||||
"page": window.cowyo.pageName
|
||||
}),
|
||||
type: 'DELETE',
|
||||
success: function() {
|
||||
window.location.reload(true);
|
||||
}
|
||||
});
|
||||
event.target.classList.add('deleting');
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Avoid uploading the same thing twice (check if it's already present while allowing failed uploads to be overwritten?)
|
||||
function onUploadFinished(file) {
|
||||
this.removeFile(file);
|
||||
var cursorPos = $('#userInput').prop('selectionStart');
|
||||
var cursorEnd = $('#userInput').prop('selectionEnd');
|
||||
var v = $('#userInput').val();
|
||||
var textBefore = v.substring(0, cursorPos);
|
||||
var textAfter = v.substring(cursorPos, v.length);
|
||||
var message = 'uploaded file';
|
||||
if (cursorEnd > cursorPos) {
|
||||
message = v.substring(cursorPos, cursorEnd);
|
||||
textAfter = v.substring(cursorEnd, v.length);
|
||||
}
|
||||
var prefix = '';
|
||||
if (file.type.startsWith("image")) {
|
||||
prefix = '!';
|
||||
}
|
||||
var extraText = prefix+'['+file.xhr.getResponseHeader("Location").split('filename=')[1]+'](' +
|
||||
file.xhr.getResponseHeader("Location") +
|
||||
')';
|
||||
|
||||
$('#userInput').val(
|
||||
textBefore +
|
||||
extraText +
|
||||
textAfter
|
||||
);
|
||||
|
||||
// Select the newly-inserted link
|
||||
$('#userInput').prop('selectionStart', cursorPos);
|
||||
$('#userInput').prop('selectionEnd', cursorPos + extraText.length);
|
||||
$('#userInput').trigger('keyup'); // trigger a save
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
@ -22,525 +21,226 @@
|
|||
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#fff">
|
||||
|
||||
<script type="text/javascript" src="/static/js/jquery-1.8.3.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/github-markdown.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/menus-min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/base-min.css">
|
||||
<link rel="stylesheet" href="/static/css/highlight.css">
|
||||
<script src="/static/js/highlight.min.js"></script>
|
||||
<script type="text/javascript" src="/static/js/highlight.pack.js"></script>
|
||||
|
||||
<style type="text/css">
|
||||
{{ if .ListPage }}
|
||||
/* Required for lists */
|
||||
span { cursor: pointer; }
|
||||
{{ end }}
|
||||
body {
|
||||
background: #fff;
|
||||
}
|
||||
.success {
|
||||
color: #5cb85c;
|
||||
font-weight: bold;
|
||||
}
|
||||
.failure {
|
||||
color: #d9534f;
|
||||
font-weight: bold;
|
||||
}
|
||||
.pure-menu a {
|
||||
color: #777;
|
||||
}
|
||||
#wrap {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
}
|
||||
#pad {
|
||||
height:100%;
|
||||
}
|
||||
{{ if .EditPage }}
|
||||
body {
|
||||
overflow:hidden;
|
||||
}
|
||||
{{ end }}
|
||||
body#pad textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
border: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
resize: none;
|
||||
font-size: 1.0em;
|
||||
{{ if .HasDotInName }}
|
||||
font-family: "Lucida Console", Monaco, monospace;
|
||||
{{else}}
|
||||
font-family: 'Open Sans','Segoe UI',Tahoma,Arial,sans-serif;
|
||||
{{ end }}
|
||||
}
|
||||
|
||||
.markdown-body ul, .markdown-body ol {
|
||||
padding-left: 0em;
|
||||
}
|
||||
|
||||
@media (min-width: 5em) {
|
||||
div#menu, div#rendered, body#pad textarea {
|
||||
padding-left: 2%;
|
||||
padding-right: 2%;
|
||||
}
|
||||
.pure-menu .pure-menu-horizontal {
|
||||
max-width: 300px;
|
||||
}
|
||||
.pure-menu-disabled, .pure-menu-heading, .pure-menu-link {
|
||||
padding-left:1.2em;
|
||||
padding-right:em;
|
||||
}
|
||||
}
|
||||
@media (min-width: 50em) {
|
||||
div#menu, div#rendered, body#pad textarea {
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
.pure-menu-disabled, .pure-menu-heading, .pure-menu-link {
|
||||
padding: .5em 1em;
|
||||
}
|
||||
}
|
||||
@media (min-width: 70em) {
|
||||
div#menu, div#rendered, body#pad textarea {
|
||||
padding-left: 15%;
|
||||
padding-right: 15%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 100em) {
|
||||
div#menu, div#rendered, body#pad textarea {
|
||||
padding-left: 20%;
|
||||
padding-right: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
{{ if and .CustomCSS .ReadPage }}
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/custom.css">
|
||||
{{ else }}
|
||||
<link rel="stylesheet" href="/static/css/dropzone.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/github-markdown.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/menus-min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/base-min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/highlight.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/default.css">
|
||||
<script type="text/javascript" src="/static/js/jquery-1.8.3.js"></script>
|
||||
<script src="/static/js/highlight.min.js"></script>
|
||||
<script type="text/javascript" src="/static/js/highlight.pack.js"></script>
|
||||
<script src="/static/js/dropzone.js"></script>
|
||||
{{ end }}
|
||||
|
||||
<title>{{ .Page }}</title>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script type='text/javascript'>
|
||||
oulipo = false;
|
||||
//<![CDATA[
|
||||
$(window).load(function() {
|
||||
// Returns a function, that, as long as it continues to be invoked, will not
|
||||
// be triggered. The function will be called after it stops being called for
|
||||
// N milliseconds. If `immediate` is passed, trigger the function on the
|
||||
// leading edge, instead of the trailing.
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
$('#saveEditButton').removeClass()
|
||||
$('#saveEditButton').text("Editing");
|
||||
var context = this,
|
||||
args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
};
|
||||
|
||||
// This will apply the debounce effect on the keyup event
|
||||
// And it only fires 500ms or half a second after the user stopped typing
|
||||
$('#userInput').on('keyup', debounce(function() {
|
||||
console.log('typing occurred');
|
||||
if (oulipo == true) { $('#userInput').val($('#userInput').val().replace(/e/g,"")); }
|
||||
$('#saveEditButton').removeClass()
|
||||
$('#saveEditButton').text("Saving")
|
||||
upload();
|
||||
}, 500));
|
||||
|
||||
function upload() {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/update',
|
||||
data: JSON.stringify({
|
||||
new_text: $('#userInput').val(),
|
||||
page: "{{ .Page }}"
|
||||
}),
|
||||
success: function(data) {
|
||||
$('#saveEditButton').removeClass()
|
||||
if (data.success == true) {
|
||||
$('#saveEditButton').addClass("success");
|
||||
} else {
|
||||
$('#saveEditButton').addClass("failure");
|
||||
}
|
||||
$('#saveEditButton').text(data.message);
|
||||
},
|
||||
error: function(xhr, error) {
|
||||
$('#saveEditButton').removeClass()
|
||||
$('#saveEditButton').addClass("failure");
|
||||
$('#saveEditButton').text(error);
|
||||
},
|
||||
contentType: "application/json",
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
|
||||
function primeForSelfDestruct() {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/prime',
|
||||
data: JSON.stringify({
|
||||
page: "{{ .Page }}"
|
||||
}),
|
||||
success: function(data) {
|
||||
$('#saveEditButton').removeClass()
|
||||
if (data.success == true) {
|
||||
$('#saveEditButton').addClass("success");
|
||||
} else {
|
||||
$('#saveEditButton').addClass("failure");
|
||||
}
|
||||
$('#saveEditButton').text(data.message);
|
||||
},
|
||||
error: function(xhr, error) {
|
||||
$('#saveEditButton').removeClass()
|
||||
$('#saveEditButton').addClass("failure");
|
||||
$('#saveEditButton').text(error);
|
||||
},
|
||||
contentType: "application/json",
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
|
||||
function lockPage(passphrase) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/lock',
|
||||
data: JSON.stringify({
|
||||
page: "{{ .Page }}",
|
||||
passphrase: passphrase
|
||||
}),
|
||||
success: function(data) {
|
||||
$('#saveEditButton').removeClass()
|
||||
if (data.success == true) {
|
||||
$('#saveEditButton').addClass("success");
|
||||
} else {
|
||||
$('#saveEditButton').addClass("failure");
|
||||
}
|
||||
$('#saveEditButton').text(data.message);
|
||||
if (data.success == true && $('#lockPage').text() == "Lock") {
|
||||
window.location = "/{{ .Page }}/view";
|
||||
}
|
||||
if (data.success == true && $('#lockPage').text() == "Unlock") {
|
||||
window.location = "/{{ .Page }}/edit";
|
||||
}
|
||||
},
|
||||
error: function(xhr, error) {
|
||||
$('#saveEditButton').removeClass()
|
||||
$('#saveEditButton').addClass("failure");
|
||||
$('#saveEditButton').text(error);
|
||||
},
|
||||
contentType: "application/json",
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
|
||||
function encryptPage(passphrase) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/encrypt',
|
||||
data: JSON.stringify({
|
||||
page: "{{ .Page }}",
|
||||
passphrase: passphrase
|
||||
}),
|
||||
success: function(data) {
|
||||
$('#saveEditButton').removeClass()
|
||||
if (data.success == true) {
|
||||
$('#saveEditButton').addClass("success");
|
||||
} else {
|
||||
$('#saveEditButton').addClass("failure");
|
||||
}
|
||||
$('#saveEditButton').text(data.message);
|
||||
if (data.success == true && $('#encryptPage').text() == "Encrypt") {
|
||||
window.location = "/{{ .Page }}/view";
|
||||
}
|
||||
if (data.success == true && $('#encryptPage').text() == "Decrypt") {
|
||||
window.location = "/{{ .Page }}/edit";
|
||||
}
|
||||
},
|
||||
error: function(xhr, error) {
|
||||
$('#saveEditButton').removeClass()
|
||||
$('#saveEditButton').addClass("failure");
|
||||
$('#saveEditButton').text(error);
|
||||
},
|
||||
contentType: "application/json",
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
|
||||
function clearOld() {
|
||||
$.ajax({
|
||||
type: 'DELETE',
|
||||
url: '/oldlist',
|
||||
data: JSON.stringify({
|
||||
page: "{{ .Page }}"
|
||||
}),
|
||||
success: function(data) {
|
||||
$('#saveEditButton').removeClass()
|
||||
if (data.success == true) {
|
||||
$('#saveEditButton').addClass("success");
|
||||
} else {
|
||||
$('#saveEditButton').addClass("failure");
|
||||
}
|
||||
$('#saveEditButton').text(data.message);
|
||||
if (data.success == true) {
|
||||
window.location = "/{{ .Page }}/list";
|
||||
}
|
||||
},
|
||||
error: function(xhr, error) {
|
||||
$('#saveEditButton').removeClass();
|
||||
$('#saveEditButton').addClass("failure");
|
||||
$('#saveEditButton').text(error);
|
||||
},
|
||||
contentType: "application/json",
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$("#encryptPage").click(function(e) {
|
||||
e.preventDefault();
|
||||
var passphrase = prompt("Please enter a passphrase. Note: Encrypting will remove all previous history.", "");
|
||||
if (passphrase != null) {
|
||||
encryptPage(passphrase);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$("#erasePage").click(function(e) {
|
||||
e.preventDefault();
|
||||
var r = confirm("Are you sure you want to erase?");
|
||||
if (r == true) {
|
||||
window.location = "/{{ .Page }}/erase";
|
||||
} else {
|
||||
x = "You pressed Cancel!";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$("#selfDestructPage").click(function(e) {
|
||||
e.preventDefault();
|
||||
var r = confirm("This will erase the page the next time it is opened, are you sure you want to do that?");
|
||||
if (r == true) {
|
||||
primeForSelfDestruct();
|
||||
} else {
|
||||
x = "You pressed Cancel!";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$("#lockPage").click(function(e) {
|
||||
e.preventDefault();
|
||||
var passphrase = prompt("Please enter a passphrase to lock", "");
|
||||
if (passphrase != null) {
|
||||
if ($('#lockPage').text() == "Lock") {
|
||||
$('#saveEditButton').removeClass();
|
||||
$("#saveEditButton").text("Locking");
|
||||
} else {
|
||||
$('#saveEditButton').removeClass();
|
||||
$("#saveEditButton").text("Unlocking");
|
||||
}
|
||||
lockPage(passphrase);
|
||||
// POST encrypt page
|
||||
// reload page
|
||||
}
|
||||
});
|
||||
|
||||
$("#clearOld").click(function(e) {
|
||||
e.preventDefault();
|
||||
var r = confirm("This will erase all cleared list items, are you sure you want to do that? (Versions will stay in history).");
|
||||
if (r == true) {
|
||||
clearOld()
|
||||
} else {
|
||||
x = "You pressed Cancel!";
|
||||
}
|
||||
});
|
||||
|
||||
$("textarea").keydown(function(e) {
|
||||
if(e.keyCode === 9) { // tab was pressed
|
||||
// get caret position/selection
|
||||
var start = this.selectionStart;
|
||||
var end = this.selectionEnd;
|
||||
|
||||
var $this = $(this);
|
||||
var value = $this.val();
|
||||
|
||||
// set textarea value to: text before caret + tab + text after caret
|
||||
$this.val(value.substring(0, start)
|
||||
+ "\t"
|
||||
+ value.substring(end));
|
||||
|
||||
// put caret at right position again (add one for the tab)
|
||||
this.selectionStart = this.selectionEnd = start + 1;
|
||||
|
||||
// prevent the focus lose
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
$('.deletable').click(function(event) {
|
||||
event.preventDefault();
|
||||
var lineNum = $(this).attr('id')
|
||||
$.ajax({
|
||||
url: "/listitem" + '?' + $.param({
|
||||
"lineNum": lineNum,
|
||||
"page": "{{ .Page }}"
|
||||
}),
|
||||
type: 'DELETE',
|
||||
success: function() {
|
||||
window.location.reload(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
}); //]]>
|
||||
|
||||
hljs.initHighlightingOnLoad();
|
||||
window.cowyo = {
|
||||
debounceMS: {{ .Debounce }},
|
||||
lastFetch: {{ .UnixTime }},
|
||||
pageName: "{{ .Page }}",
|
||||
}
|
||||
</script>
|
||||
<script>hljs.initHighlightingOnLoad();</script>
|
||||
|
||||
<script type="text/javascript" src="/static/js/cowyo.js"></script>
|
||||
</head>
|
||||
|
||||
<body id="pad">
|
||||
<body id="pad" class="
|
||||
{{ if .EditPage }} EditPage {{ end }}
|
||||
{{ if .ViewPage }} ViewPage {{ end }}
|
||||
{{ if .ListPage }} ListPage {{ end }}
|
||||
{{ if .HistoryPage }} HistoryPage {{ end }}
|
||||
{{ if .ReadPage }} ReadPage {{ end }}
|
||||
{{ if .DontKnowPage }} DontKnowPage {{ end }}
|
||||
{{ if .DirectoryPage }} DirectoryPage {{ end }}
|
||||
{{ if .HasDotInName }} HasDotInName {{ end }}
|
||||
">
|
||||
<article class="markdown-body">
|
||||
<div class="pure-menu pure-menu-horizontal" id="menu">
|
||||
<ul class="pure-menu-list">
|
||||
<li></li>
|
||||
<!-- Required to keep them level? -->
|
||||
<li class="pure-menu-item pure-menu-has-children pure-menu-allow-hover">
|
||||
<a href="#" id="menuLink1" class="pure-menu-link">{{ .Page }}</a>
|
||||
<ul class="pure-menu-children">
|
||||
<li class="pure-menu-item"><a href="/" class="pure-menu-link">New</a></li>
|
||||
<li class="pure-menu-item"><a href="https://github.com/schollz/cowyo" class="pure-menu-link">Source</a></li>
|
||||
<hr>
|
||||
{{ if (or (.IsLocked) (.IsEncrypted) )}}
|
||||
{{ else }}
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" id="encryptPage">{{ if .IsEncrypted }}Decrypt{{ else }}Encrypt{{end}}</a></li>
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" id="lockPage">{{ if .IsLocked }}Unlock{{ else }}Lock{{end}}</a></li>
|
||||
<li class="pure-menu-item"><a href="/{{ .Page }}/history" class="pure-menu-link">History</a></li>
|
||||
<hr>
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" id="selfDestructPage">Self-destruct</a></li>
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" id="erasePage">Erase</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</li>
|
||||
<!--
|
||||
<li class="pure-menu-item {{ with .ViewPage }}pure-menu-selected{{ end }}">
|
||||
<a href="/{{ .Page }}/view" class="pure-menu-link">View</a>
|
||||
</li>-->
|
||||
|
||||
<li class="pure-menu-item pure-menu-has-children pure-menu-allow-hover {{ with .ViewPage }}pure-menu-selected{{ end }}">
|
||||
<a href="#" id="menuLink1" class="pure-menu-link">View</a>
|
||||
<ul class="pure-menu-children">
|
||||
<li class="pure-menu-item">
|
||||
<a href="/{{ .Page }}/view" class="pure-menu-link">Current</a>
|
||||
{{ if .ReadPage }}
|
||||
<!-- No menu for read page -->
|
||||
{{ else }}
|
||||
<div class="pure-menu pure-menu-horizontal" id="menu">
|
||||
<ul class="pure-menu-list">
|
||||
<li></li>
|
||||
<!-- Required to keep them level? -->
|
||||
<li class="pure-menu-item pure-menu-has-children pure-menu-allow-hover">
|
||||
<a href="#" id="menuLink1" class="pure-menu-link">{{ .Page }}</a>
|
||||
<ul class="pure-menu-children">
|
||||
{{ if .DiaryMode }}
|
||||
<li class="pure-menu-item"><a href="/{{ .Date }}/edit" class="pure-menu-link">New</a></li>
|
||||
{{ else }}
|
||||
<li class="pure-menu-item"><a href="/" class="pure-menu-link">New</a></li>
|
||||
{{ end }}
|
||||
<li class="pure-menu-item"><a href="https://github.com/schollz/cowyo" class="pure-menu-link">Source</a></li>
|
||||
{{ if .EditPage }}
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" id="publishPage">
|
||||
{{- if .IsPublished -}}
|
||||
Unpublish
|
||||
{{- else -}}
|
||||
Publish
|
||||
{{- end -}}
|
||||
</a></li>
|
||||
{{ end }}
|
||||
<hr>
|
||||
{{ if (or (.IsLocked) (.IsEncrypted) )}}
|
||||
{{ else }}
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" id="encryptPage">{{ if .IsEncrypted }}Decrypt{{ else }}Encrypt{{end}}</a></li>
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" id="lockPage">{{ if .IsLocked }}Unlock{{ else }}Lock{{end}}</a></li>
|
||||
<li class="pure-menu-item"><a href="/{{ .Page }}/history" class="pure-menu-link">History</a></li>
|
||||
<hr>
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" id="selfDestructPage">Self-destruct</a></li>
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" id="erasePage">Erase</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</li>
|
||||
{{ range .RecentlyEdited}}
|
||||
<li class="pure-menu-item"><a class="pure-menu-link" href="/{{.}}/view/">{{.}}</a></li>
|
||||
|
||||
<li class="pure-menu-item pure-menu-has-children pure-menu-allow-hover {{ with .ViewPage }}pure-menu-selected{{ end }}">
|
||||
<a href="/{{ .Page }}/view" class="pure-menu-link">View</a>
|
||||
<ul class="pure-menu-children">
|
||||
<li class="pure-menu-item">
|
||||
<a href="/{{ .Page }}/read" class="pure-menu-link">Current</a>
|
||||
</li>
|
||||
{{ range .RecentlyEdited}}
|
||||
<li class="pure-menu-item"><a class="pure-menu-link" href="/{{.}}/read">{{.}}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
{{ if (or (.IsLocked) (.IsEncrypted) )}}
|
||||
{{ if .IsLocked }}
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" id="lockPage">{{ if .IsLocked }}Unlock{{ else }}Lock{{end}}</a></li>
|
||||
<li class="pure-menu-item" class="pure-menu-link"><a href="#"><span id="saveEditButton"></span></a></li>
|
||||
{{ else }}
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" id="encryptPage">{{ if .IsEncrypted }}Decrypt{{ else }}Encrypt{{end}}</a></li>
|
||||
<li class="pure-menu-item" class="pure-menu-link"><a href="#"><span id="saveEditButton"></span></a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
{{ if (or (.IsLocked) (.IsEncrypted) )}}
|
||||
{{ if .IsLocked }}
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" id="lockPage">{{ if .IsLocked }}Unlock{{ else }}Lock{{end}}</a></li>
|
||||
<li class="pure-menu-item" class="pure-menu-link"><a href="#"><span id="saveEditButton"></span></a></li>
|
||||
{{ else }}
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" id="encryptPage">{{ if .IsEncrypted }}Decrypt{{ else }}Encrypt{{end}}</a></li>
|
||||
<li class="pure-menu-item" class="pure-menu-link"><a href="#"><span id="saveEditButton"></span></a></li>
|
||||
{{ end }}
|
||||
{{else}}
|
||||
{{ if .ListPage }}
|
||||
<li class="pure-menu-item {{ with .ListPage }}pure-menu-selected{{ end }}"><a href="#" class="pure-menu-link" id="clearOld">Clear done</a></li>
|
||||
{{ else }}
|
||||
<li class="pure-menu-item {{ with .ListPage }}pure-menu-selected{{ end }}"><a href="/{{ .Page }}/list" class="pure-menu-link">List</a></li>
|
||||
{{ end }}
|
||||
<li class="pure-menu-item {{ with .EditPage }}pure-menu-selected{{ end }}"><a href="/{{ .Page }}/edit" class="pure-menu-link"><span id="saveEditButton">Edit</span></a></li>
|
||||
{{end}}
|
||||
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{else}}
|
||||
{{ if .ListPage }}
|
||||
<li class="pure-menu-item {{ with .ListPage }}pure-menu-selected{{ end }}"><a href="#" class="pure-menu-link" id="clearOld">Clear done</a></li>
|
||||
{{ else }}
|
||||
<li class="pure-menu-item {{ with .ListPage }}pure-menu-selected{{ end }}"><a href="/{{ .Page }}/list" class="pure-menu-link">List</a></li>
|
||||
{{ end }}
|
||||
<li class="pure-menu-item {{ with .EditPage }}pure-menu-selected{{ end }}"><a href="/{{ .Page }}/edit" class="pure-menu-link"><span id="saveEditButton">Edit</span></a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div id="wrap">
|
||||
{{ if .EditPage }} <div id="pad"><textarea autofocus placeholder="Start typing, it will save automatically.
|
||||
{{ if .EditPage }}
|
||||
|
||||
Use Markdown for formatting and links (also make links like [[this]])." id="userInput">{{ .RawPage }}</textarea></div>{{ end }}
|
||||
<div id="rendered">
|
||||
{{ if .DontKnowPage }} <strong><center>{{ .Route }} not understood!</center></strong>{{ end }}
|
||||
{{ if .ViewPage }}{{ .RenderedPage }}{{ end }}
|
||||
{{ if .HistoryPage }}
|
||||
<h1>History</h1>
|
||||
<ul>
|
||||
{{range $i, $e := .Versions}}
|
||||
<li style="list-style: none;">
|
||||
<a href="/{{ $.Page }}/view?version={{$e}}">View</a> <a href="/{{ $.Page }}/list?version={{$e}}">List</a> <a href="/{{ $.Page }}/raw?version={{$e}}">Raw</a>
|
||||
{{index $.VersionsText $i}} ({{if lt (index $.VersionsChangeSums $i) 0}}<span style="color:red">{{else}}<span style="color:green">+{{end}}{{index $.VersionsChangeSums $i}}</span>)</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{ end }}
|
||||
{{ if .ListPage }}
|
||||
{{ range $index, $element := .ListItems }}
|
||||
{{ $element }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<div id="pad">
|
||||
|
||||
{{ if .DirectoryPage }}
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Document</th>
|
||||
<th>Current size</th>
|
||||
<th>Num Edits</th>
|
||||
<th>Last Edited</th>
|
||||
</tr>
|
||||
{{range $i, $e := .FileNames}}
|
||||
<tr>
|
||||
<td><a href="/{{ $e }}/view">{{ $e }}</a></td>
|
||||
<td>{{index $.FileSizes $i}}</td>
|
||||
<td>{{index $.FileNumChanges $i}}</td>
|
||||
<td>{{index $.FileLastEdited $i}}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
{{end}}
|
||||
</div>
|
||||
<script>
|
||||
Dropzone.options.userInputForm = {
|
||||
clickable: false,
|
||||
maxFilesize: {{ if .MaxUploadMB }} {{.MaxUploadMB}} {{ else }} 10 {{end }}, // MB
|
||||
init: function initDropzone() {
|
||||
this.on("complete", onUploadFinished);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<form
|
||||
id="userInputForm"
|
||||
action="/uploads"
|
||||
{{ if .AllowFileUploads }}
|
||||
class="dropzone"
|
||||
{{ end }}
|
||||
>
|
||||
<textarea
|
||||
autofocus
|
||||
placeholder="Use markdown to write your note! New: you can publish your note when you are done ({{ .Page }} -> Publish)."
|
||||
id="userInput"
|
||||
>{{ .RawPage }}</textarea>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div id="rendered">
|
||||
{{ if .DontKnowPage }}
|
||||
<strong>
|
||||
<center>
|
||||
{{ .Route }} not understood!
|
||||
</center>
|
||||
</strong>
|
||||
{{ end }}
|
||||
|
||||
{{ if .ViewPage }}
|
||||
{{ .RenderedPage }}
|
||||
{{ end }}
|
||||
|
||||
{{ if .ReadPage }}
|
||||
{{ .RenderedPage }}
|
||||
{{ end }}
|
||||
|
||||
{{ if .HistoryPage }}
|
||||
<h1>History</h1>
|
||||
<ul>
|
||||
{{range $i, $e := .Versions}}
|
||||
<li style="list-style: none;">
|
||||
<a href="/{{ $.Page }}/view?version={{$e}}">View</a>
|
||||
|
||||
<a href="/{{ $.Page }}/list?version={{$e}}">List</a>
|
||||
|
||||
<a href="/{{ $.Page }}/raw?version={{$e}}">Raw</a>
|
||||
|
||||
{{index $.VersionsText $i}} ({{if lt (index $.VersionsChangeSums $i) 0}}<span style="color:red">{{else}}<span style="color:green">+{{end}}{{index $.VersionsChangeSums $i}}</span>)</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{ end }}
|
||||
|
||||
{{ if .ListPage }}
|
||||
{{ range $index, $element := .ListItems }}
|
||||
{{ $element }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if .DirectoryPage }}
|
||||
<table style="width:100%">
|
||||
{{ $upload := .UploadPage }}
|
||||
<tr>
|
||||
<th>Document</th>
|
||||
<th>Current size</th>
|
||||
{{ if not $upload }}
|
||||
<th>Num Edits</th>
|
||||
{{ end }}
|
||||
<th>Last Edited</th>
|
||||
</tr>
|
||||
{{range .DirectoryEntries}}
|
||||
<tr>
|
||||
<td>
|
||||
{{ if $upload }}
|
||||
<a href="/uploads/{{ .Name }}">{{ sniffContentType .Name }}</a>
|
||||
{{ else }}
|
||||
<a href="/{{ .Name }}/view">{{ .Name }}</a>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>{{.Size}}</td>
|
||||
{{ if not $upload }}
|
||||
<td>{{.Numchanges}}</td>
|
||||
{{ end }}
|
||||
<td>{{.ModTime.Format "Mon Jan 2 15:04:05 MST 2006" }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ if .ChildPageNames }}
|
||||
<section class="ChildPageNames">
|
||||
<h2>See also</h2>
|
||||
<ul>
|
||||
{{ range .ChildPageNames }}
|
||||
<li><a href="/{{ . }}/view">{{ . }}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</section>
|
||||
{{ end }}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
Loading…
Reference in New Issue