Compare commits

...

142 Commits

Author SHA1 Message Date
Zack Scholl ed996d9e09 update deps 2023-07-06 09:56:50 -07:00
Zack a58f4ca346
Create FUNDING.yml 2020-05-14 10:17:15 -07:00
Zack Scholl a318863eca fix dockerfile 2019-07-06 08:57:39 -06:00
Zack Scholl d8d3200491 update dockerfile 2019-07-05 06:59:34 -07:00
Zack Scholl f414ccc2f3 update readme 2019-07-05 06:55:56 -07:00
Zack Scholl 0a9b069152 update dockerfile 2019-07-05 06:54:48 -07:00
Zack Scholl 4954276644 update deps 2019-07-05 06:45:52 -07:00
Zack Scholl 156722d23e use go.mod 2019-07-05 06:42:41 -07:00
Zack Scholl 9accd685c0 update depndencies 2018-08-22 21:14:49 -06:00
Zack Scholl 126b13fcea update cookiestore 2018-08-21 19:36:24 -07:00
Zack Scholl 5a0587c3ce update cookie store 2018-08-21 19:36:10 -07:00
Zack Scholl e5a1a88a02 update docker 2018-07-31 12:56:00 -07:00
Zack Scholl d43cc80b53 update dependencies 2018-07-31 12:40:32 -07:00
Zack Scholl ff84acd9d7 make sure to publish 2018-07-21 14:36:10 -07:00
Zack 1e46c40cc6
Merge pull request #137 from DanielHeath/toml
Configure via Toml
2018-07-21 14:32:44 -07:00
Daniel Heath 9e928f6b74 Fix bindata for build 2018-05-28 10:25:30 +10:00
Daniel Heath 3b91b699e3 Add 'tomlo', configure cowyo via TOML 2018-05-27 21:51:33 +10:00
Daniel Heath f567f86bab Remove unused property 2018-05-27 21:49:55 +10:00
Daniel Heath 0e93250cc9 Remove dud log line 2018-05-27 21:49:43 +10:00
Daniel Heath 82d5ac908d Always name the cookie "_session" 2018-05-27 21:49:34 +10:00
Daniel Heath 7a61b16e7a Add TOML library 2018-05-27 21:49:05 +10:00
Daniel Heath f86dc1095a Make the max document length configurable 2018-05-02 16:27:35 +10:00
Daniel Heath a976007642 Fixup: Debug mode broken 2018-04-30 20:19:11 +10:00
Zack 180a28cda6
Merge pull request #133 from DanielHeath/master
Make cowyo separately mountable
2018-04-30 02:19:23 -07:00
Daniel Heath 08bbce1298 Move main back to root 2018-04-28 19:55:15 +10:00
Daniel Heath e53123d8fa Add herdyo command for an example of a multisite 2018-04-28 19:54:44 +10:00
Daniel Heath d48d1458a5 Make save mutex per-site 2018-04-28 19:32:48 +10:00
Daniel Heath d9b8bfc95d Remove final global var 2018-04-28 19:29:35 +10:00
Daniel Heath bc1a9ee86b Remove global logger 2018-04-28 12:30:13 +10:00
Daniel Heath f4f5042245 Remove sitemapUpToDate global 2018-04-28 12:19:43 +10:00
Daniel Heath d095915f83 One more global 2018-04-28 12:16:10 +10:00
Daniel Heath c2cd54a12d Killing globals 2018-04-28 12:13:19 +10:00
Daniel Heath 2e80633cd4 Kill more globals 2018-04-28 12:12:10 +10:00
Daniel Heath 9cf0d0e129 Remove maxupload global 2018-04-28 12:10:47 +10:00
Daniel Heath ede4d1fba3 Pass in a session store so they can be shared 2018-04-28 12:09:08 +10:00
Daniel Heath ff5c100cf8 Fixup tests 2018-04-28 11:58:26 +10:00
Daniel Heath 465e9c8e93 Move serving and CSS reading out of Site 2018-04-28 11:56:07 +10:00
Daniel Heath 0d984da5b2 Move serve params into a struct 2018-04-28 11:42:51 +10:00
Daniel Heath 8944170646 Initial split (towards a separately mountable cowyo) 2018-04-28 11:32:08 +10:00
Zack Scholl f21c89f7bd update dependencies 2018-04-22 05:51:19 -06:00
Zack Scholl 91178f4f29 bug fix: need to add custom funcs when using assets 2018-04-19 20:59:14 -07:00
Zack Scholl 8bc1123f1d display the filename when uploading 2018-04-19 20:55:08 -07:00
Zack Scholl 5d0523117b bump version 2018-04-18 00:10:38 -07:00
Zack Scholl 6e1d64b40e bump version 2018-04-18 00:09:28 -07:00
Zack f9b8713404
Merge pull request #129 from DanielHeath/master
Fix sitemaps, make brute-forcing access codes harder
2018-04-18 00:05:06 -07:00
Daniel Heath fc3030339f Add "/uploads" to list all uploaded files 2018-04-01 11:31:26 +10:00
Daniel Heath df406ec71b Raw pages are always shown text/plain (fix #127) 2018-04-01 10:06:51 +10:00
Daniel Heath 2a43ebdb53 Upgrade dependencies 2018-03-31 15:30:04 +11:00
Daniel Heath d9e622fa9d Sitemap includes the request host/port, fixes #125 2018-03-31 14:34:15 +11:00
Daniel Heath 446ba00fe9 Remove .DS_Store from bindata 2018-03-31 14:34:15 +11:00
Daniel Heath f729aece51 Run gofmt when building assets 2018-03-31 14:34:14 +11:00
Daniel Heath e5dfacb4bb Update to latest teeny-security: Makes brute-forcing access codes much harder 2018-03-31 14:34:14 +11:00
Zack 2373e339d0
Merge pull request #119 from katomaso/feature/lock-per-session
Lock page with respect to session
2018-03-26 23:19:53 -07:00
Tomas Peterka c64e54a991 Lock page with respect to session 2018-03-11 23:46:29 +01:00
Zack f350d0bbad
Update README.md 2018-02-20 07:30:13 -08:00
Zack 308244cfa4
Update README.md 2018-02-20 07:29:59 -08:00
Zack 50624e75e9
Merge pull request #117 from DanielHeath/implement-file-uploads
Implement file upload / download, fix #77
2018-02-20 05:54:09 -08:00
Daniel Heath efd4cfb0d1 Implement file upload / download, fix #77 2018-02-20 21:04:33 +11:00
Zack Scholl 4ebf9e02ef Allow center 2018-02-18 12:04:23 -08:00
Zack Scholl c1a78c2006 can not -> cannot 2018-02-18 11:59:46 -08:00
Zack Scholl 8a4ba13506 Change default stylesheet 2018-02-18 11:50:02 -08:00
Zack c78dec28eb
Merge pull request #115 from DanielHeath/split-bindata
Remove unused cgo and split debug/non-debug asset builds
2018-02-18 11:34:39 -08:00
Zack 0f8e572d43
Merge pull request #116 from DanielHeath/run-gofmt
Run gofmt
2018-02-18 11:34:16 -08:00
Zack Scholl 1720711029 Self destruct with published messages works 2018-02-18 11:33:32 -08:00
Zack Scholl 3c53386e08 Debounce works 2018-02-18 11:33:23 -08:00
Zack 9d4fcae7b2
Merge pull request #118 from DanielHeath/tidy-logging-statement
Remove logging statement
2018-02-15 19:30:56 -08:00
Daniel Heath db48f1291b Remove unused cgo and split debug/non-debug asset builds
It's now much harder to accidentally build a release with debug or old assets.
2018-02-16 10:54:41 +11:00
Daniel Heath 131a54a682 Gofmt 2018-02-16 10:53:59 +11:00
Daniel Heath bb475bd924 Remove logging statement 2018-02-16 10:53:01 +11:00
Zack dfd9aea863
Merge pull request #113 from DanielHeath/master
Make the 'view' link do something helpful
2018-02-09 15:13:08 -08:00
Daniel Heath 9982fb5175 Auth doesn't stop you publishing stuff 2018-02-07 15:09:01 +11:00
Daniel Heath f5f0bdb3bb Prevent updates from running out-of-order 2018-02-07 14:48:10 +11:00
Daniel Heath d65eccbe9d Whitespace 2018-01-31 22:38:40 +11:00
Daniel Heath 1a3e891dfd Fixup test 2018-01-31 21:44:15 +11:00
Daniel Heath 917d32f8a5 Support category pages 2018-01-31 16:47:12 +11:00
Daniel Heath c207077877 Enable hot-template-reloading in development 2018-01-31 16:47:12 +11:00
Daniel Heath 5e4a317b10 Split inline CSS & javascript into their own files 2018-01-31 16:47:12 +11:00
Daniel Heath da6d6e5d43 Avoid overwriting others work (fix #107) 2018-01-31 16:47:10 +11:00
Daniel Heath d0bc74ec55 Only save the page if its contents have changed 2018-01-31 16:46:58 +11:00
Daniel Heath b42750073c Whitespace 2018-01-31 16:46:58 +11:00
Daniel Heath e1934b9797 Clicking "View" while editing takes you to the current view page 2018-01-31 16:46:58 +11:00
Daniel Heath 0badf719e0 Add mutex around Save, fixes #70 2018-01-31 16:46:57 +11:00
Zack a0dcc9652f
Merge pull request #108 from DanielHeath/mutex-save
Add mutex around page.Save(), fixes #70
2018-01-27 07:56:39 -05:00
Zack fb6405ba1b
Merge pull request #109 from DanielHeath/fix-comment
Fix comment: Upload the right binary
2018-01-27 07:56:16 -05:00
Daniel Heath b18f40e336 Add mutex around Save, fixes #70 2018-01-27 20:58:20 +11:00
Daniel Heath fa66472128 Upload the binary you built (ok, it's just a comment) 2018-01-27 20:27:33 +11:00
Zack 526688c7e3
Merge pull request #104 from DanielHeath/update-deps
Update all dependencies to latest
2018-01-23 18:41:25 -05:00
Zack 31153063f2
Merge pull request #103 from DanielHeath/fix-gitignore
Fixup gitignore
2018-01-23 18:40:49 -05:00
Daniel Heath b4638476cc Update all dependencies to latest 2018-01-24 08:45:42 +11:00
Zack ff420fb81d
Update Makefile 2018-01-23 09:28:57 -05:00
Daniel Heath dec21a80c2 Fixup gitignore 2018-01-22 21:04:15 +11:00
Zack f998015f0c
Merge pull request #99 from DanielHeath/master
Fade-out items while waiting for a delete to go through
2018-01-21 09:09:55 -05:00
Daniel Heath fea5ef4647 Merge branch 'master' of github.com:danielheath/cowyo 2018-01-21 12:52:43 +11:00
Daniel Heath ecf1d2ab10 Add password to access wiki 2018-01-21 12:09:16 +11:00
Daniel Heath 765c5788b3 Ignore dist 2018-01-21 12:03:55 +11:00
Daniel Heath 76ec1c1acb Allow setting session secret cookie code 2018-01-18 20:51:08 +11:00
Daniel Heath 89d58f5a22 Allow insecure markup (for private wikis amongst friends) 2018-01-18 20:50:55 +11:00
Daniel Heath 2f1c0e3cd2 Whitespace 2018-01-18 20:50:13 +11:00
Daniel Heath 10d45b0c76 Add my deploy instructions 2018-01-18 17:39:11 +11:00
Daniel Heath 038a895772 Sort 'ls' page by recent 2018-01-18 17:39:05 +11:00
Daniel Heath ff2920965d Sort 'ls' page by recent 2018-01-18 17:12:03 +11:00
Daniel Heath a36f8e318e Use a struct for related data instead of four separate arrays 2018-01-18 17:08:04 +11:00
Daniel Heath c802fa06be Strip trailing whitespace 2018-01-18 17:04:44 +11:00
Daniel Heath 81f6b2d263 Remove random println 2018-01-03 22:47:27 +11:00
Daniel Heath 5803cbdc3f Fade-out items while waiting for a delete to go through 2018-01-03 22:47:13 +11:00
Daniel Heath 5a82e77738 Strip trailing whitespace 2018-01-03 22:38:39 +11:00
Zack Scholl 55368b1c1a Fix bug with one letter routes 2017-11-13 11:32:32 -07:00
Zack Scholl 7cc1eccb83 Add 32-bit version 2017-11-05 06:35:11 -07:00
Zack Scholl 45436a3762 Update the readme 2017-11-04 16:15:57 -06:00
Zack Scholl 684ff4e692 Add -diary for diary mode
New creates a new page with a timestamp
2017-11-04 16:09:10 -06:00
Zack Scholl 63bdbb5824 Add -debounce for setting the debounce time 2017-11-04 15:30:44 -06:00
Zack Scholl 85877602e0 /view -> /read when published
Fixes #97
2017-11-04 15:23:38 -06:00
Zack Scholl 6eacf90e18 Sitemap should goto /read and not /view 2017-11-04 12:57:04 -06:00
Zack Scholl 4a916984e6 Need to handle /page/ and /page the same
Fixes #88
2017-11-04 12:49:02 -06:00
Zack Scholl 840fbf73ec Don't need this line anymore 2017-11-04 18:45:10 +00:00
Zack Scholl a2288cf129 Oops 2017-11-04 04:52:29 -06:00
Zack Scholl 8ccd40c105 Update README 2017-11-04 04:52:18 -06:00
Zack Scholl 1ef24a0347 Update dependencies 2017-11-04 04:45:08 -06:00
Zack 8b3e7b0605
Merge pull request #93 from schollz/v3
V3
2017-11-04 04:42:49 -06:00
Zack Scholl de03d2b547 Add -lock for automatic locking
Fixes #90
2017-11-04 04:41:56 -06:00
Zack Scholl d481145c5f Add default page Fixes #91 2017-11-04 04:21:51 -06:00
Zack Scholl 2040dbaaa5 Add -css option for custom CSS on the /read page 2017-11-04 04:18:05 -06:00
Zack Scholl 5ae5c91945 Trying to make a /read 2017-11-02 06:27:39 -06:00
Zack 0ef75a919c Update README.md 2017-10-15 09:00:21 -06:00
Zack da30d15eb1 Merge pull request #84 from yamilurbina/adding_dockerfile
Added Dockerfile to generate a small docker image
2017-10-15 08:50:44 -06:00
Zack 9d22d5d41d Update README.md 2017-10-15 08:38:31 -06:00
Zack Scholl c35aeca859 Generate sitemap in thread because it takes a long time 2017-10-15 08:18:48 -06:00
Zack Scholl e52d0097a9 Adding new bindata 2017-10-15 07:59:07 -06:00
Zack Scholl b2a01d0a6d Sitemap now working 2017-10-15 07:58:43 -06:00
Zack Scholl 026dd7c647 Remove twitter 2017-10-15 07:50:13 -06:00
Zack Scholl 107a0dc32b Add publishing, for sitemap.xml 2017-10-15 07:49:40 -06:00
Zack 683bcdea1c Update README.md 2017-10-12 17:11:36 -06:00
Zack e33ddcfddb Update README.md 2017-10-12 17:10:45 -06:00
Zack Scholl 6baa87daa4 Update bindata with twitter 2017-10-12 17:08:38 -06:00
Zack 912bf83c59 Merge pull request #86 from benjaminmisell/master
Add twitter share icon
2017-10-12 13:53:14 -06:00
Benjamin Misell ecba3aa0ea Add twitter share icon 2017-10-12 20:29:48 +01:00
Yamil Urbina b77c32d646
Improvements for Dockerfile; docker-compose file added 2017-10-11 00:26:33 -04:00
Zack Scholl bef20f3366 Vendoring 2017-10-03 14:43:55 -04:00
Zack 21047443d6 Merge pull request #85 from tgulacsi/master
Small tweaks
2017-10-03 06:45:27 -04:00
Tamás Gulácsi b83e3fd52e precompile regexp 2017-10-02 13:58:39 +02:00
Tamás Gulácsi 8474b79cf1 migrate: return on error (esp. Save) 2017-10-02 13:55:48 +02:00
Zack Scholl b3b5f31575 Bump version 2017-09-30 09:18:30 -04:00
34 changed files with 8221 additions and 1327 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,3 @@
# These are supported funding model platforms
github: schollz

5
.gitignore vendored
View File

@ -22,3 +22,8 @@ _testmain.go
*.exe
*.test
*.prof
data/*
cowyo
dist

13
Dockerfile Normal file
View File

@ -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"]

View File

@ -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

View File

@ -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

40
cmd/herdyo/herdyo.go Normal file
View File

@ -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)
}
})))
}

14
cmd/tomlo/tomlo.go Normal file
View File

@ -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())
}

55
config/config.go Normal file
View File

@ -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()
}
}

102
config/defaults.go Normal file
View File

@ -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)
}
}

105
config/http.go Normal file
View File

@ -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)
}

9
docker-compose.yml Normal file
View File

@ -0,0 +1,9 @@
version: "2"
services:
cowyo:
build: .
ports:
- 8050:8050
volumes:
- ./data:/data

35
go.mod Normal file
View File

@ -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
)

405
go.sum Normal file
View File

@ -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=

View File

@ -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
View File

@ -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)
}

View File

@ -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
}

40
multisite_sample.toml Normal file
View File

@ -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
View File

@ -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"))
}

View File

@ -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)
}
}

1174
server/bindata-debug.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

10
server/debug.go Normal file
View File

@ -0,0 +1,10 @@
// +build debug
package server
import "github.com/jcelliott/lumber"
func init() {
hotTemplateReloading = true
LogLevel = lumber.TRACE
}

889
server/handlers.go Executable file
View File

@ -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"})
}

View File

@ -1,4 +1,4 @@
package main
package server
import (
"html/template"

36
server/migrate.go Executable file
View File

@ -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
}

207
server/page.go Executable file
View File

@ -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
}

74
server/page_test.go Executable file
View File

@ -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])
}
}

View File

@ -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) {

View File

@ -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")
}
}

128
static/css/default.css Normal file
View File

@ -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;
}

388
static/css/dropzone.css Normal file
View File

@ -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; }

388
static/js/cowyo.js Normal file
View File

@ -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
}

3504
static/js/dropzone.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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>&nbsp;&nbsp;<a href="/{{ $.Page }}/list?version={{$e}}">List</a>&nbsp;&nbsp;<a href="/{{ $.Page }}/raw?version={{$e}}">Raw</a>&nbsp;&nbsp;
{{index $.VersionsText $i}}&nbsp;({{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>
&nbsp;&nbsp;
<a href="/{{ $.Page }}/list?version={{$e}}">List</a>
&nbsp;&nbsp;
<a href="/{{ $.Page }}/raw?version={{$e}}">Raw</a>
&nbsp;&nbsp;
{{index $.VersionsText $i}}&nbsp;({{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>