Started Plugin Rewrite

This commit is contained in:
Brian Buller 2019-01-10 10:39:42 -06:00
parent 5096f3522e
commit e0f25b8529
28 changed files with 1697 additions and 681 deletions

428
assets.go Normal file
View File

@ -0,0 +1,428 @@
// Code generated by "esc -o assets.go assets templates"; DO NOT EDIT.
package main
import (
"bytes"
"compress/gzip"
"encoding/base64"
"io/ioutil"
"net/http"
"os"
"path"
"sync"
"time"
)
type _escLocalFS struct{}
var _escLocal _escLocalFS
type _escStaticFS struct{}
var _escStatic _escStaticFS
type _escDirectory struct {
fs http.FileSystem
name string
}
type _escFile struct {
compressed string
size int64
modtime int64
local string
isDir bool
once sync.Once
data []byte
name string
}
func (_escLocalFS) Open(name string) (http.File, error) {
f, present := _escData[path.Clean(name)]
if !present {
return nil, os.ErrNotExist
}
return os.Open(f.local)
}
func (_escStaticFS) prepare(name string) (*_escFile, error) {
f, present := _escData[path.Clean(name)]
if !present {
return nil, os.ErrNotExist
}
var err error
f.once.Do(func() {
f.name = path.Base(name)
if f.size == 0 {
return
}
var gr *gzip.Reader
b64 := base64.NewDecoder(base64.StdEncoding, bytes.NewBufferString(f.compressed))
gr, err = gzip.NewReader(b64)
if err != nil {
return
}
f.data, err = ioutil.ReadAll(gr)
})
if err != nil {
return nil, err
}
return f, nil
}
func (fs _escStaticFS) Open(name string) (http.File, error) {
f, err := fs.prepare(name)
if err != nil {
return nil, err
}
return f.File()
}
func (dir _escDirectory) Open(name string) (http.File, error) {
return dir.fs.Open(dir.name + name)
}
func (f *_escFile) File() (http.File, error) {
type httpFile struct {
*bytes.Reader
*_escFile
}
return &httpFile{
Reader: bytes.NewReader(f.data),
_escFile: f,
}, nil
}
func (f *_escFile) Close() error {
return nil
}
func (f *_escFile) Readdir(count int) ([]os.FileInfo, error) {
return nil, nil
}
func (f *_escFile) Stat() (os.FileInfo, error) {
return f, nil
}
func (f *_escFile) Name() string {
return f.name
}
func (f *_escFile) Size() int64 {
return f.size
}
func (f *_escFile) Mode() os.FileMode {
return 0
}
func (f *_escFile) ModTime() time.Time {
return time.Unix(f.modtime, 0)
}
func (f *_escFile) IsDir() bool {
return f.isDir
}
func (f *_escFile) Sys() interface{} {
return f
}
// FS returns a http.Filesystem for the embedded assets. If useLocal is true,
// the filesystem's contents are instead used.
func FS(useLocal bool) http.FileSystem {
if useLocal {
return _escLocal
}
return _escStatic
}
// Dir returns a http.Filesystem for the embedded assets on a given prefix dir.
// If useLocal is true, the filesystem's contents are instead used.
func Dir(useLocal bool, name string) http.FileSystem {
if useLocal {
return _escDirectory{fs: _escLocal, name: name}
}
return _escDirectory{fs: _escStatic, name: name}
}
// FSByte returns the named file from the embedded assets. If useLocal is
// true, the filesystem's contents are instead used.
func FSByte(useLocal bool, name string) ([]byte, error) {
if useLocal {
f, err := _escLocal.Open(name)
if err != nil {
return nil, err
}
b, err := ioutil.ReadAll(f)
_ = f.Close()
return b, err
}
f, err := _escStatic.prepare(name)
if err != nil {
return nil, err
}
return f.data, nil
}
// FSMustByte is the same as FSByte, but panics if name is not present.
func FSMustByte(useLocal bool, name string) []byte {
b, err := FSByte(useLocal, name)
if err != nil {
panic(err)
}
return b
}
// FSString is the string version of FSByte.
func FSString(useLocal bool, name string) (string, error) {
b, err := FSByte(useLocal, name)
return string(b), err
}
// FSMustString is the string version of FSMustByte.
func FSMustString(useLocal bool, name string) string {
return string(FSMustByte(useLocal, name))
}
var _escData = map[string]*_escFile{
"/assets/css/main.css": {
local: "assets/css/main.css",
size: 4980,
modtime: 1547133837,
compressed: `
H4sIAAAAAAAC/7RXW2vcOBR+n18hCIWkVK4nzTSJA2GhF/Zh96mPpQ+yJdsismRkeTJpyX9fdLMlW54u
CwsJjHUuOpfvXNSqjoFfOwA6ymFLaNOqAuzz/M3D7nW3KwV+MdRKMCELcHF7e/uwxYwMpyInBTGphESK
Cl4ALjgxdEyPWSW4IlwZzh5hTHlTgP2hP6W1TheXDFVPk5YWsdqoeKZYtQU4OAuyliBMpPUHyYZyyEit
CpBbco3gtSHWgis40J+kANeks8Re0g7J2N08v70jtxEdlo1hKVH11EgxcgxT3ERKISNdnz/neZ4H1DOa
Al7tr+h1KBGrKWE48G62fkd5P6qspozEsc0OpAPZR80GAKZDz9BLAShnlBNYMqGjCkApJCayAPv+BAbB
KAYXVVVZygkOLcLiWUsNRIHccH3oT+ACYzxLQ4kwHYcC3OhsvnqTJOJPlDfQfMVJs1lfSB/C08iiT58+
PXiAIUYbXoCKcEWkvS3rxJFAWgluwz7KQceSD1ASnWrDddERPoZWeJx53N04q3oxUItfSRhS9EgeolQV
4GJ/v7/b3+njZ1I+UQXFkciaiWc4VFIwZuKvxFi1wdVZP0oC9U+owUq5xcBGZlJiXGiWp1RZxlnfL5I+
ZXujRAF4bqkicOhRRfThs0R9aIO5vhyVciFeGB2HDZWDYKMyp0r0BXCBlTbO7stVqWHYx2fSNwJ76lw1
Jj6c7TPOTqGU6KJULxBnL53w5TQv3NUNC1HumsrksY9Y2t/YR3OPx1Wi2D2Otm5+HNlWnwhETV8Ukv7U
YsxB0MhNsDwVwB7bgumaTLVjV3JELWPQe30mTquGrM+i2tERn/RsxWvC3lJdwqkvX74EYLZpus5dU8nc
cdzlDp5qsBt1yJgEB4rJsB4QhxT0FqJHIhWtEAuljXGxsAVeIM2QbMhy7OzvPLmkDVybvZ8c1gbOk9U1
Pn2oyZkxdUU2p4ZuO+SKIWicjgUiB5cUGwDe+TXFW4xGJYxChUpG/MVKrxCPCp+zwAr4xDreVW4NF1C1
ENbfdBXbKWGAojum7VZwnsZnhu3cTr9+/brWoUTTMAJrKoelARYw0wiL24zFQ4rlNXlBRzF2U3wxGuP+
aIELo3npDtfhSF3E0NoRLX/ejxTHf7EoMqnAdNDpxVs5crPff97f37t+KTBicFpTFyW499hxfLoTMmRZ
j3SgJWVUvRSgpRgTvt3P3RIZtHP3e2OJ8N/pUvoJKcfkZNjydA+UTYku83fA/WWHq4Qbj5geFz3jjf4P
DLvzPdsz5P3JFWoyzAb4yeXLmxoj0g/n1Sq/UemYHi9IV9KmIZzgb5UkhH9rXUH7ja0X1Idqy8Qzqh5p
1/yK5lQ4uVyGpsmlp2Y9MjYY+eUUfJPI8e83nBgtSQiEPXPKh8fFPs/3QYXUQnbAbM/ffZX8KLhQl9/V
S09+XGX+VNcSNLD27WMzwf7zcDjom+ymbuaUDDemm9xPoT86gikCl/qJNhFJd2W456V6vWut4xmGsKYn
Yp4Qr7tJz2rNTKxdK/Zp7VgIUE4VRewhss0ZYpSslsWUCwmDF2nWlWF7jjuwyoP36KJLTtoNo4EhQ0Pr
WMP36c1ZK0LgpT2cdAdv0t/MwWgSOsgsdA1jVZFh2NZ2fX2P6vttba+73fu34Fun1w5bvQN4+94DzdUj
4hhczpX84aAfNldutTza+tCpl4JBfX8PGCoJy/yZ+XI2rtcnbQYAmeDsBc5LWhJunm3o/JoUsGFSo5HN
ChVBHewQRw3pCFcOy8PajsiMDanHcE7GSLKrj33lTQFVSCq9RiFlg/sOzK+CrQDPRf0/RjgZujnCIR7+
nC1WAvxNMB27f2/7AjTg5s54BOwNTttvIBe0ubuEzo83ocq/bONcqwz1BCL/BAAA///ACu/RdBMAAA==
`,
},
"/assets/vendor/css/grids-responsive-min.css": {
local: "assets/vendor/css/grids-responsive-min.css",
size: 8032,
modtime: 1547133812,
compressed: `
H4sIAAAAAAAC/8SVT4/bRg/G7/4UziHA+wa1LM9oRrL3UjTNoUBQFMipR68trIXqjyHLSbaL/e6FIc1G
fEjOIqc9PhQ1Esnh81t/eLf469qXy69p4hOz+NidH/vq4TQsTbqxy7/3p657t/hcHcr2Uh6X1/ZY9svh
VC5/+/L7cgoni9MwnC+79fqhGk7X++TQNevH25vr87Uv1/d1d79u9peh7Nef//j46c8vn5LmuPiwXvza
lMdqv7wc+rJsl/v2uPxfU7Wrb9VxOO2sS1zZ/P8puR2yuq4uzWrzy1wwaahGmVFtqYSnjkpPZUFkimez
f2FfNxiwGMgw4DDgMZBjoMDAFgKGaUslaYPBQg3WZdiBWJfBuizTIB2VpPV4WEazHcwBW+gw3VNJvoXt
zuHwnD0nr+MsbqN4OlaXc71/3FVtXbXl6r7uDv/c/dt1zW5zV5fDUPary3l/qNqHXdv1zb6++9b1R4x9
LfuhOuzr1b6uHtrd0J3vhvL7sOrL27reEvfXoXuGbXga1yxLNt7n758jq2R+ZBeJtdZCdoHznJI3JnGQ
6nF6IdUnnv+Gm56alDxwP94zaVLwH8pwbiGb/k4+e7KV2mBxgFO2tbwNcDO3s+wc2mBeCstoYcxJbhc4
pG6EDs3n6KROGOY6U7qDL8/G5qQrkQm3P+QXUjdCiR4+xHewCJnsunhuTyFXui+zeebSvSi41YR0+PBs
erlwM7KX6gpaneED9CFTuDPMP/OXZhR4aRhUzGyaW+FymNlIt9LliMCUubR7qXiTpu+fn6Pkzoo5t5vj
7OybYNJQjTKj2lIJTx2VnsqCyBTPZv/Cvm4wYDGQYcBhwGMgx0CBgS0EDNOWStIGg4UarMuwA7Eug3VZ
pkE6Kknr8bCMZjuYA7bQYbqnknwL253D4Tl7Tl7HWbwdt6dtULgtrFKE23wbVG7zPYpxe1xCgdvTGDVu
811WuD1NTOM2t4gYt/nNVLk9rpXAbclJYtwOvqJxW/BAhdvBfzRuS3YU4/a4tgK3JRvTuC05XITbwf80
bkt2KHM7+KTG7dFmBG5LDhnhtuSfGrclqES5HaxX4/YrMGUu/VPc9tmc2/XD7OybYNJQjTKj2lIJTx2V
nsqCyBTPZv/Cvm4wYDGQYcBhwGMgx0CBgS0EDNOWStIGg4UarMuwA7Eug3VZpkE6Kknr8bCMZjuYA7bQ
YbqnknwL253D4Tl7Tl7HWbwdt6dtULgtrFKE23wbVG7zPYpxe1xCgdvTGDVu811WuD1NTOM2t4gYt/nN
VLk9rpXAbclJYtwOvqJxW/BAhdvBfzRuS3YU4/a4tgK3JRvTuC05XITbwf80bkt2KHM7+KTG7dFmBG5L
DhnhtuSfGrclqES5HaxX4/YrMGUu/VPcLtI5t7/Xs7NvgklDNcqMakslPHVUeioLIlM8m/0L+7rBgMVA
hgGHAY+BHAMFBrYQMExbKkkbDBZqsC7DDsS6DNZlmQbpqCStx8Mymu1gDthCh+meSvItbHcOh+fsOXkd
Z/F23J62QeG2sEoRbvNtULnN9yjG7XEJBW5PY9S4zXdZ4fY0MY3b3CJi3OY3U+X2uFYCtyUniXE7+IrG
bcEDFW4H/9G4LdlRjNvj2grclmxM47bkcBFuB//TuC3Zoczt4JMat0ebEbgtOWSE25J/atyWoBLldrBe
jduvwJS5NHL7vwAAAP//TZMIY2AfAAA=
`,
},
"/assets/vendor/css/pure-min.css": {
local: "assets/vendor/css/pure-min.css",
size: 16449,
modtime: 1547133812,
compressed: `
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
`,
},
"/templates/footer.html": {
local: "templates/footer.html",
size: 228,
modtime: 1547128347,
compressed: `
H4sIAAAAAAAC/1zPwQqDMAzG8btPEXIX0XPsu9SlYCFbh0kLvv3Yplh76uH/ox+hgWNxHXEsEHnGZ2Iv
fSphE7+j6wDoBwAAaB0rY9EkoKNhHc88VVnzconpEPeVJfH+zddA07NZeikesXEP8aozbkGD9e8sUn11
XvV/PgEAAP///4iBjuQAAAA=
`,
},
"/templates/header.html": {
local: "templates/header.html",
size: 200,
modtime: 1547136888,
compressed: `
H4sIAAAAAAAC/0yNsQrDIBRFd7/ikj0JZLYuhW6d2h+w+lIFayGaLPL+PRgkZHzvcM6V1m8wQad06+ag
k4OhmGlBKcOj3vfKmDslcP6elJL+ErOQo/WbEteK+cdMMbdOr0NoLvyM4bV+3j4HArMArp4jbWnpfxTX
QwCkm1Qpp8IsRzdV1FaPKEVbW3sAAAD//7XiV5HIAAAA
`,
},
"/templates/htmlfooter.html": {
local: "templates/htmlfooter.html",
size: 115,
modtime: 1547128255,
compressed: `
H4sIAAAAAAAC/1JQUFCw0U/JLLPjAjGrqxWKEvPSUxVUMnUUVMoUrGwV9IKTizILSooVamvBSmyKwXyF
4qJkW6XqapCq2lolOxt9iDjcnNS8FIgWG/2k/JRKOy4b/YyS3Bw7LkAAAAD//zPK1EBzAAAA
`,
},
"/templates/htmlheader.html": {
local: "templates/htmlheader.html",
size: 801,
modtime: 1547128244,
compressed: `
H4sIAAAAAAAC/5yST2/UMBDF7/spplaPdQxCCKjivZRKcAGk9oKqHrz2ZD2LYwd7krKK8t1R9g+70J44
JeOZ95uXF9cXH7/e3H//dgue27Bc1PsHQO3RuPkFoG6RDVhvckHWoudGvhfnrWha1GIgfOpSZgE2RcbI
WjyRY68dDmRR7ooroEhMJshiTUD9unr1AsphsZk6phTPaH8NeuZO4s+eBi1ujPUo57mcwpngS5K71lEY
KP6AjEEL03UBJafeekl23uIzNlooUwpyUdSuVWOGuVV1cS2Atx1qQa1Zo5oPnhGLT5ltz/CfuD2PiQMu
x7G6I8bqfq6mCSTMJ/3qUNdqP7ZY1BdSPlADgeHzLXx4PLjahwclWy3mnK6Vsi5uSmVD6l0TTMbKplaZ
jfmlAq2Kmv/62+JpUG+qd6eq2hSxrNWet1zUFw8YHTWPUh4MjyNkE9cIl3QFlwNca6jueBuweEQuME3P
cvrTPYY0jrNymg6ZjiNgdEflSws+oXGY73amTivOP/qEPJn/l12r4w2vV8ltD9E5GoCcFsFsU89iufgd
AAD//1puW/YhAwAA
`,
},
"/templates/login.html": {
local: "templates/login.html",
size: 812,
modtime: 1547136935,
compressed: `
H4sIAAAAAAAC/6SSTW7jMAyF9zkFwb3hC9gGZjHLwQSTuYBs0bFQSRQoKU1uX8RW/LNogaIr88mPTx8F
NtrcYLAqxhYnZUcYyCeSSlmL3QmgGVncyxCyUDUfrFWlrLl60ghqSIZ9i3WOJLXlq/EIjtLEusXz38v/
Oe+ZaMjqSGmRAHuEOXdgn4RtdRXOAV82gMaqniyMLC2SU8Zi9/v5gV9aC8XY1LNh12B8yAmMfvnBK0er
SI9ALSa6J4Rg1UATW03S4iEVQeXEIw85rsS1Nrfu9BP+oGJ8Z9HYnUv1Ff3qLgNseplh04c5zusl3wKP
nzALOXI9CR6bJhreer7vmg7sW9fCvumFfeuHf+UX/KEN4PgsZYIi+pwS+xIUc+9MOsIVw66ughin5IHd
ZfY39XJelrPeb2dTP1e8O5VLPwIAAP//0xnP9ywDAAA=
`,
},
"/templates/user_dashboard.html": {
local: "templates/user_dashboard.html",
size: 0,
modtime: 1547138339,
compressed: `
H4sIAAAAAAAC/wEAAP//AAAAAAAAAAA=
`,
},
"/": {
isDir: true,
local: "",
},
"/assets": {
isDir: true,
local: "assets",
},
"/assets/css": {
isDir: true,
local: "assets/css",
},
"/assets/vendor": {
isDir: true,
local: "assets/vendor",
},
"/assets/vendor/css": {
isDir: true,
local: "assets/vendor/css",
},
"/templates": {
isDir: true,
local: "templates",
},
}

355
assets/css/main.css Normal file
View File

@ -0,0 +1,355 @@
html {
min-height: 100%;
}
body {
color: #777;
min-height: 100%;
}
a {
text-decoration: none;
}
div.content {
padding: 15px;
min-height: 100%;
color: black;
}
div.half {
width: 50%;
}
.header {
margin-left: 0;
}
.fa-2 {
font-size: 2em;
}
.primary {
color: #0078e7;
}
.primary-bg {
background-color: #0078e7;
}
.error {
color: #DD0000;
}
.error-bg {
background-color: #DD0000;
}
div.optionalfield {
margin: 2em;
}
input.file {
padding: .5em .6em;
display: inline-block;
border: 1px solid #ccc;
box-shadow: inset 0 1px 3px #ddd;
border-radius: 4px;
}
input.ranking-input {
width: 50px;
border-radius: 5px;
border: 1px solid #CCC;
text-align: center;
}
i.move-icon {
cursor: ns-resize;
}
#menu {
width: 100%;
height: 40px;
position: relative;
background: #191818;
webkit-overflow-scrolling: touch;
}
#menu .pure-menu-heading {
display: inline-block;
}
#menu .pure-menu-nonlink {
color: #777;
padding: .5em 1em;
display: block;
text-decoration: none;
white-space: nowrap;
}
#menu .menu-button {
display: inline;
position: absolute;
top: 0px;
right: 0px;
margin-top: 10px;
margin-right: 10px;
color: white;
text-decoration: none;
}
.menu-bottom {
width: 150px;
border-top: 1px solid white;
}
#menu .menu-container {
display: none;
position: absolute;
right: 0px;
top: 40px;
background-color: #191818;
}
#menu .menu-container>ul {
background-color: #191818;
}
div.horizontal-scroll {
overflow-x: scroll;
}
img.thumbnail {
height: 100px;
max-height: 100%;
max-width: 100%;
}
.thumbnail-container {
display: block;
height: 100%;
background-color: #EEE;
padding-top: 20px;
}
.padding {
padding: 5px;
}
.space {
margin: 5px;
}
.space-sides {
margin-left: 5px;
margin-right: 5px;
}
.space-vertical {
margin-top: 5px;
margin-bottom: 5px;
}
.large {
font-size: 18px;
}
.big-space {
margin: 10px;
}
.left {
text-align: left;
}
.right {
text-align: right;
}
.center {
text-align: center;
}
.center-all {
text-align: center;
vertical-align: center;
margin: auto;
}
table.center tbody>td {
text-align: center;
}
table.padding td {
padding: 5px;
}
table tfoot {
border-top: 1px solid #CCC;
}
.pure-button-error {
background-color: #DD0000;
color: #FFF;
}
.pure-button-toggle-first {
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}
.pure-button-toggle-middle {
border-radius: 0px;
margin-left: -5px;
border-left: 1px solid #CCC;
}
.pure-button-toggle-last {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
margin-left: -5px;
border-left: 1px solid #CCC;
}
.pure-button:disabled {
background-color: #CCC;
color: #999;
}
#modal-body {
margin-bottom: 15px;
}
#modal-overlay {
visibility: hidden;
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
text-align: center;
z-index: 1000;
background-color: rgba(0, 0, 0, 0.5);
}
#modal-overlay>div {
margin: 10% 10%;
width: 80%;
margin: 100px auto;
background-color: #FFF;
border: 1px solid #000;
border-radius: 10px;
padding: 15px;
text-align: center;
}
div#embiggenedScreenShot {
cursor: pointer;
background-color: #FFF;
}
div#embiggenedScreenShot>img{
max-width:100%;
max-height:100%;
}
div.fullscreen {
height: 100%;
width: 100%;
position: absolute;
top: 0px;
left: 0px;
text-align: center;
margin: auto auto;
z-index:1001;
}
.pure-form input[disabled]:not([type]).disabled-but-visible {
background-color: #FFF;
color: #555;
}
input.larger {
width: 400px;
}
@media (min-width: 40em) {
#menu {
width: 150px;
height: 100%;
position: fixed;
}
#menu .menu-button {
display: none;
}
#menu .menu-container{
display: initial;
width: 100%;
}
.menu-bottom {
width: 150px;
position: fixed;
left: 0px;
bottom: 0px;
}
.header {
margin-left: 150px;
}
div.flash {
font-size: 24px;
position: fixed;
top: 0px;
width: 100%;
}
div.flash.error {
background-color: #DD0000;
color: #FFFFFF;
}
div.flash.success {
background-color: #229af9;
color: #FFFFFF;
}
}
/* Small Screens */
@media screen and (max-width:35.5em) {
div.pure-control-group label.control-label {
text-align: left;
}
.only-large {
display: none;
}
.only-small {
display: default;
}
.team-management-buttons {
text-align:left;
}
.team-management-buttons>.pure-button {
margin-top: 1em;
}
}
/* Starting at Small, horizontal */
@media screen and (min-width: 35.5em) {
div.pure-control-group label.control-label {
text-align: left;
}
.only-small {
display: none;
}
}
/* Small Horizontal to Medium */
@media screen and (min-width: 35.5em) and (max-width: 48em) { }
/* Medium Screens */
@media screen and (min-width: 48em) and (max-width:64em) { }
/* Larger Screens */
@media (min-width: 64em) { }

File diff suppressed because one or more lines are too long

11
assets/vendor/css/pure-min.css vendored Normal file

File diff suppressed because one or more lines are too long

8
buildplugins.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
cd plugin_src
for i in `ls`; do
go build -buildmode=plugin $i
done
cd ..
mv plugin_src/*.so plugins/

View File

@ -1,91 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/gorilla/mux"
)
func handleApiComicsCall(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
vars := mux.Vars(req)
cid, cidok := vars["cid"]
fn, fnok := vars["function"]
switch req.Method {
case "GET":
if !cidok {
handleApiListComics(w)
return
}
if !fnok {
handleApiShowComic(cid, w)
return
}
switch fn {
case "search":
handleApiSearchComic(cid, w)
return
default:
userError(w)
return
}
default:
userError(w)
return
}
}
func handleApiListComics(w http.ResponseWriter) {
var res []byte
var err error
if res, err = json.Marshal(m.Comics); err != nil {
serverError(w)
return
}
fmt.Fprint(w, string(res))
}
func handleApiSearchComic(nm string, w http.ResponseWriter) {
nm = strings.ToLower(nm)
var cs []Comic
for _, c := range m.Comics {
if strings.Contains(strings.ToLower(c.Name), nm) {
cs = append(cs, c)
}
}
var res []byte
var err error
if res, err = json.Marshal(cs); err != nil {
serverError(w)
return
}
fmt.Fprint(w, string(res))
}
func handleApiShowComic(cid string, w http.ResponseWriter) {
var err error
var c *Comic
pts := strings.Split(cid, ";")
if len(pts) != 2 {
userError(w)
return
}
if c, err = m.GetComic(pts[0], pts[1]); err != nil {
userError(w)
return
}
var res []byte
if res, err = json.Marshal(c); err != nil {
userError(w)
return
}
fmt.Fprint(w, string(res))
return
}

View File

@ -125,7 +125,7 @@ func handleApiSubUser(uid string, slug string, w http.ResponseWriter) {
userError(w)
return
}
_, err = m.GetComic(pts[0], pts[1])
//_, err = m.GetComic(pts[0], pts[1])
if err != nil {
userError(w)
return

View File

@ -8,31 +8,30 @@ import (
)
func handleRequest(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
page := initPageData(w, req)
var fOk, uidOk bool
var f, uid string
f, fOk = vars["function"]
uid, uidOk = vars["uid"]
if !fOk || !uidOk {
// Not sure what you want me to do here, Hoss.
http.Error(w, "You did a bad", 400)
return
}
switch f {
case "rss":
handleRssFeed(uid, w)
default:
http.Error(w, "You did a bad", 400)
//vars := mux.Vars(req)
//var fOk, uidOk bool
//var f, uid string
//f, fOk = vars["function"]
//uid, uidOk = vars[""]
if !page.LoggedIn {
handleUserLoginForm(page)
}
}
func handleRssFeed(uid string, w http.ResponseWriter) {
func handleRssFeed(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
var uid string
var uidOk bool
if uid, uidOk = vars["uid"]; !uidOk {
userError(w)
return
}
w.Header().Set("Content-Type", "application/xml")
v, err := buildRssFeed(uid)
if err != nil {
http.Error(w, err.Error(), 400)
userError(w)
return
}
fmt.Fprint(w, v)

69
endpoints_user.go Normal file
View File

@ -0,0 +1,69 @@
package main
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/gorilla/mux"
)
func handleUserRequest(w http.ResponseWriter, req *http.Request) {
page := initPageData(w, req)
vars := mux.Vars(req)
f, fOk := vars["function"]
if fOk && f == "login" {
handleUserLogin(page)
return
}
if !page.LoggedIn {
page.SubTitle = "ribbit login"
page.show("login.html", w)
return
}
page.SubTitle = "dashboard"
page.show("user_dashboard.html", w)
}
func handleUserLoginForm(page *pageData) {
page.SubTitle = "ribbit login"
page.show("login.html", page.session.w)
}
func handleUserLogin(page *pageData) {
var err error
var u *User
if page.LoggedIn {
redirect("/", page.session.w, page.session.req)
}
email := page.session.req.FormValue("email")
if u, err = m.GetUserByEmail(email); err != nil {
page.session.setFlashMessage("Invalid Login", "error")
redirect("/", page.session.w, page.session.req)
return
}
password := page.session.req.FormValue("password")
fmt.Println("Hitting func")
if err := doLogin(u.Uuid, password); err != nil {
fmt.Println("Login Failure: ", err.Error())
page.session.setFlashMessage("Invalid Login", "error")
} else {
page.session.setStringValue("id", u.Uuid)
}
redirect("/", page.session.w, page.session.req)
}
// doLogin attempts to log in with the given email/password
// If it can't, it returns an error
func doLogin(uid, password string) error {
fmt.Println("Doing Login", uid, password)
if strings.TrimSpace(uid) != "" && strings.TrimSpace(password) != "" {
return m.checkCredentials(uid, password)
}
return errors.New("Invalid Credentials")
}

View File

@ -1,74 +0,0 @@
package main
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/PuerkitoBio/goquery"
)
func downloadXKCDList() []Comic {
var ret []Comic
ret = append(ret, *NewComic("xkcd", "XKCD", "Randall Munroe", "xkcd"))
return ret
}
func getXKCDRssItem(slug string) (string, error) {
desc, err := getXKCDFeedDesc(time.Now())
if err != nil {
return "", err
}
comic, err := m.GetComic(SRC_XKCD, slug)
if err != nil {
return "", err
}
desc = "<![CDATA[" + desc + "]]>"
ret := " <item>\n"
ret += " <title>" + comic.Name + "</title>\n"
ret += " <pubDate>" + comic.LastUpdate.Format(time.RFC1123Z) + "</pubDate>\n"
ret += " <guid>xkcd;" + slug + ";" + comic.LastUpdate.Format(time.RFC1123Z) + "</guid>\n"
ret += " <link>" + getXKCDComicUrl(time.Now()) + "</link>\n"
ret += " <description>" + desc + "</description>\n"
ret += " </item>\n"
return ret, nil
}
func getXKCDComicUrl(date time.Time) string {
// TODO: Actually make this work correctly
// Get the previous comic number
// and find the next one
return fmt.Sprintf(
"http://xkcd.com/",
)
}
func getXKCDFeedDesc(date time.Time) (string, error) {
res, err := http.Get(getXKCDComicUrl(date))
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return "", errors.New(fmt.Sprintf("Status code error: %d %s", res.StatusCode, res.Status))
}
// Load the HTML document
doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil {
return "", err
}
// Find the Picture
sel := doc.Find("div#comic>img")
src, exists := sel.Attr("src")
if !exists {
return "", errors.New("Couldn't find image source")
}
src = "https:" + src
title, exists := sel.Attr("title")
if !exists {
title = ""
}
return "<img src=\"" + src + "\" /><p>" + title + "</p>", nil
}

View File

@ -1,98 +1,9 @@
package main
import (
"errors"
"net/http"
"strings"
"time"
)
const (
SRC_GOCOMICS = "gocomics"
SRC_DILBERT = "dilbert"
SRC_XKCD = "xkcd"
)
func downloadComicsList() []Comic {
var ret []Comic
ret = append(ret, downloadGoComicsList()...)
ret = append(ret, downloadDilbertList()...)
ret = append(ret, downloadXKCDList()...)
return ret
}
func getRssItem(source, slug string) (string, error) {
switch source {
case SRC_GOCOMICS:
return getGoComicsRssItem(slug)
case SRC_DILBERT:
return getDilbertRssItem(slug)
case SRC_XKCD:
return getXKCDRssItem(slug)
}
return "", errors.New("Invalid source")
}
func getComicUrl(source, slug string, dt time.Time) (string, error) {
switch source {
case SRC_GOCOMICS:
return getGoComicsComicUrl(slug, dt), nil
case SRC_DILBERT:
return getDilbertComicUrl(dt), nil
case SRC_XKCD:
return getXKCDComicUrl(dt), nil
}
return "", errors.New("Invalid source")
}
func getComicDesc(source, slug string, dt time.Time) (string, error) {
switch source {
case SRC_GOCOMICS:
return getGoComicsFeedDesc(slug, dt)
case SRC_DILBERT:
return getDilbertFeedDesc(dt)
case SRC_XKCD:
return getXKCDFeedDesc(dt)
}
return "", errors.New("Unknown Comic Source")
}
func buildRssFeed(uid string) (string, error) {
var usr *User
var err error
if usr, err = m.GetUser(uid); err != nil {
return "", err
}
output := []string{
"<?xml version=\"1.0\"?>",
"<rss version=\"2.0\">",
" <channel>",
" <title>BCW Comic Feed</title>",
" <link>http://ribbit.bullercodeworks.com/edit/" + uid + "</link>",
" <description>Comic feed for " + usr.Username + "</description>",
" <language>en-us</language>",
" <lastBuildDate>" + time.Now().Format(time.RFC1123) + "</lastBuildDate>",
" <ttl>40</ttl>",
}
//date := time.Now()
for _, slug := range usr.SubSlugs {
pts := strings.Split(slug, ";")
if len(pts) != 2 {
continue
}
if comic, err := m.GetComic(pts[0], pts[1]); err == nil {
output = append(output, comic.GetRssItem())
}
}
output = append(output, []string{
" </channel>",
"</rss>",
}...)
return strings.Join(output, "\n"), nil
}
func addStringIfUnique(st string, sl []string) []string {
for i := range sl {
if sl[i] == st {

279
main.go
View File

@ -1,7 +1,11 @@
package main
//go:generate esc -o assets.go assets templates
import (
"bufio"
"fmt"
"html/template"
"log"
"net/http"
"os"
@ -9,17 +13,42 @@ import (
"strconv"
"strings"
"syscall"
"time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/justinas/alice"
"golang.org/x/crypto/ssh/terminal"
)
const AppName = "ribbit"
const DbName = AppName + ".db"
// pageData is stuff that changes per request
type pageData struct {
Site *SiteData
Title string
SubTitle string
Stylesheets []string
HeaderScripts []string
Scripts []string
FlashMessage string
FlashClass string
LoggedIn bool
IsAdmin bool
Menu []menuItem
BottomMenu []menuItem
session *pageSession
TemplateData interface{}
}
type menuItem struct {
Label string
Location string
Icon string
}
var sessionStore *sessions.CookieStore
var r *mux.Router
var m *model
@ -32,106 +61,59 @@ func main() {
errorExit("Unable to initialize Model: " + err.Error())
}
if len(os.Args) > 2 {
key, val := os.Args[1], os.Args[2]
switch key {
case "--add-user-sub":
if len(os.Args) < 3 {
errorExit("Usage: --add-user-sub <username> <comic-slug>\nFor a list of slugs, use --list-comics")
}
slug := os.Args[3]
var u *User
if u, err = m.GetUserByName(val); err != nil {
errorExit("Couldn't find a user with the username " + val)
}
pts := strings.Split(slug, ";")
if len(pts) != 2 {
errorExit("Invalid slug given: " + slug)
}
_, err := m.GetComic(pts[0], pts[1])
if err != nil {
errorExit("Couldn't find comic with slug: " + slug)
}
fmt.Println(u.SubSlugs)
fmt.Println(slug)
u.SubSlugs = append(u.SubSlugs, slug)
fmt.Println(u.SubSlugs)
m.SaveUser(u)
done()
default:
errorExit("Unknown argument")
}
} else if len(os.Args) > 1 {
switch os.Args[1] {
case "--test":
fmt.Println(buildRssFeed("30f14e57-6500-443c-8c77-f352788eacb0"))
done()
case "--list-comics":
comics := m.GetAllComics()
for _, c := range comics {
fmt.Printf("[ %s;%s ] %s\n", c.Source, c.Slug, c.Name)
}
done()
case "--update-feeds":
fmt.Println("Updating User Feeds...")
m.UpdateAllUserFeeds()
fmt.Println("Done.")
done()
case "--update-comics":
fmt.Println("Updating the Comics List...")
comics := downloadComicsList()
for _, c := range comics {
fmt.Printf("Updating [ %s - %s, %s ]\n", c.Slug, c.Name, c.Artist)
m.SaveComic(&c)
}
m.saveChanges()
fmt.Println("Done.")
initialize()
sessionStore = sessions.NewCookieStore([]byte(m.Site.sessionSecret))
default:
errorExit("Unknown argument")
for _, arg := range os.Args {
switch arg {
case "-dev":
m.Site.DevMode = true
fmt.Println("Running in Dev Mode")
}
}
r = mux.NewRouter()
r.StrictSlash(true)
//r.PathPrefix("/assets/").Handler(http.FileServer())
r.PathPrefix("/assets/").Handler(http.FileServer(FS(m.Site.DevMode)))
pub := r.PathPrefix("/").Subrouter()
pub.HandleFunc("/", handleRequest)
pub.HandleFunc("/api", handleApiCall)
pub.HandleFunc("/api/users", handleApiUsersCall)
pub.HandleFunc("/api/users/{uid}", handleApiUsersCall)
pub.HandleFunc("/api/users/{uid}/{function}", handleApiUsersCall)
pub.HandleFunc("/api/users/{uid}/{function}/{slug}", handleApiUsersCall)
pub.HandleFunc("/api/comics", handleApiComicsCall)
pub.HandleFunc("/api/comics/{cid}", handleApiComicsCall)
pub.HandleFunc("/api/comics/{cid}/{function}", handleApiComicsCall)
pub.HandleFunc("/{function}", handleRequest)
pub.HandleFunc("/{function}/{uid}", handleRequest)
pub.HandleFunc("/{function}/{uid}/{subfunc}", handleRequest)
pub.HandleFunc("/{function}/{uid}/{subfunc}/{slug}", handleRequest)
pub.HandleFunc("/", handleUserRequest)
/*
pub.HandleFunc("/api", handleApiCall)
pub.HandleFunc("/api/users", handleApiUsersCall)
pub.HandleFunc("/api/users/{uid}", handleApiUsersCall)
pub.HandleFunc("/api/users/{uid}/{function}", handleApiUsersCall)
pub.HandleFunc("/api/users/{uid}/{function}/{slug}", handleApiUsersCall)
pub.HandleFunc("/api/comics", handleApiComicsCall)
pub.HandleFunc("/api/comics/{cid}", handleApiComicsCall)
pub.HandleFunc("/api/comics/{cid}/{function}", handleApiComicsCall)
*/
pub.HandleFunc("/rss/{uid}", handleRssFeed)
pub.HandleFunc("/user/{function}", handleUserRequest)
http.Handle("/", r)
chain := alice.New(loggingHandler).Then(r)
// Refresh the DB at 2 AM
go func() {
for {
if m.Site.LastSave.IsZero() || (time.Now().Day() != m.Site.LastSave.Day() && time.Now().Hour() == 2) {
fmt.Println("Updating GoComics List...")
comics := downloadComicsList()
for _, c := range comics {
fmt.Printf("Updating [ %s - %s, %s ]\n", c.Slug, c.Name, c.Artist)
m.SaveComic(&c)
/*
go func() {
for {
if m.Site.LastSave.IsZero() || (time.Now().Day() != m.Site.LastSave.Day() && time.Now().Hour() == 2) {
fmt.Println("Updating GoComics List...")
comics := downloadComicsList()
for _, c := range comics {
fmt.Printf("Updating [ %s - %s, %s ]\n", c.Slug, c.Name, c.Artist)
m.SaveComic(&c)
}
fmt.Println("Updating User Feeds...")
m.UpdateAllUserFeeds()
m.saveChanges()
fmt.Println("Done.")
}
fmt.Println("Updating User Feeds...")
m.UpdateAllUserFeeds()
m.saveChanges()
fmt.Println("Done.")
time.Sleep(time.Minute)
}
time.Sleep(time.Minute)
}
}()
}()
*/
// Set up a channel to intercept Ctrl+C for graceful shutdowns
c := make(chan os.Signal, 1)
@ -148,10 +130,97 @@ func main() {
log.Fatal(http.ListenAndServe("127.0.0.1:"+strconv.Itoa(m.Site.Port), chain))
}
func buildRssFeed(uid string) (string, error) {
return uid, nil
}
func loggingHandler(h http.Handler) http.Handler {
return handlers.LoggingHandler(os.Stdout, h)
}
func initPageData(w http.ResponseWriter, req *http.Request) *pageData {
if m.Site.DevMode {
w.Header().Set("Cache-Control", "no-cache")
}
p := new(pageData)
// Get session
var err error
var s *sessions.Session
if s, err = sessionStore.Get(req, m.Site.sessionSecret); err != nil {
fmt.Println("Session error... Recreating.")
http.Error(w, err.Error(), 500)
}
p.session = new(pageSession)
p.session.session = s
p.session.req = req
p.session.w = w
// First check if we're logged in
userId, _ := p.session.getStringValue("id")
// With a valid account
user, err := m.GetUser(userId)
if err == nil {
p.LoggedIn = true
p.IsAdmin = user.IsAdmin
}
p.Site = m.Site
p.SubTitle = "ribbit"
p.Stylesheets = make([]string, 0, 0)
p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/pure-min.css")
p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/grids-responsive-min.css")
p.Stylesheets = append(p.Stylesheets, "/assets/css/main.css")
p.HeaderScripts = make([]string, 0, 0)
p.Scripts = make([]string, 0, 0)
p.FlashMessage, p.FlashClass = p.session.getFlashMessage()
if p.FlashClass == "" {
p.FlashClass = "hidden"
}
// Build the menu
if p.LoggedIn {
p.BottomMenu = append(p.BottomMenu, menuItem{"Logout", "/user/logout", "fa-sign-out"})
} else {
if p.IsAdmin {
p.BottomMenu = append(p.BottomMenu, menuItem{"Admin", "/admin", "fa-sign-in"})
}
}
return p
}
func (p *pageData) show(tmplName string, w http.ResponseWriter) error {
for _, tmpl := range []string{
"htmlheader.html",
"header.html",
tmplName,
"footer.html",
"htmlfooter.html",
} {
if err := outputTemplate(tmpl, p, w); err != nil {
fmt.Printf("%s\n", err)
return err
}
}
return nil
}
func outputTemplate(tmplName string, tmplData interface{}, w http.ResponseWriter) error {
n := "/templates/" + tmplName
l := template.Must(template.New("layout").Parse(FSMustString(m.Site.DevMode, n)))
t := template.Must(l.Parse(FSMustString(m.Site.DevMode, n)))
return t.Execute(w, tmplData)
}
// redirect can be used only for GET redirects
func redirect(url string, w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, url, 303)
}
func done() {
os.Exit(0)
}
@ -166,3 +235,41 @@ func assertError(err error) {
panic(err)
}
}
func initialize() {
// Check if we have an admin user already
if !m.hasAdminUser() {
// Nope, create one
reader := bufio.NewReader(os.Stdin)
fmt.Println("Create new Admin user")
fmt.Print("Email: ")
email, _ := reader.ReadString('\n')
email = strings.TrimSpace(email)
u := NewUser(email)
var pw1, pw2 []byte
for string(pw1) != string(pw2) || string(pw1) == "" {
fmt.Print("Password: ")
pw1, _ = terminal.ReadPassword(0)
fmt.Println("")
fmt.Print("Repeat Password: ")
pw2, _ = terminal.ReadPassword(0)
fmt.Println("")
if string(pw1) != string(pw2) {
fmt.Println("Entered Passwords don't match!")
}
}
u.IsAdmin = true
m.SaveUser(u)
assertError(m.updateUserPassword(u.Uuid, string(pw1)))
}
if m.Site.sessionSecret == "" {
reader := bufio.NewReader(os.Stdin)
fmt.Println("A good session secret is like a good password")
fmt.Print("Create New Session Secret: ")
sessSc, _ := reader.ReadString('\n')
sessSc = strings.TrimSpace(sessSc)
m.Site.sessionSecret = sessSc
assertError(m.Site.SaveToDB())
}
}

View File

@ -13,8 +13,8 @@ type model struct {
bolt *boltease.DB
dbFileName string
Users []User
Comics []Comic
Users []User
Feeds []Feed
Site *SiteData
}
@ -31,11 +31,14 @@ func NewModel() (*model, error) {
if err = m.initDB(); err != nil {
return nil, errors.New("Unable to initialzie DB: " + err.Error())
}
m.LoadSiteData()
m.Site = NewSiteData(m)
if err = m.Site.LoadFromDB(); err != nil {
return nil, err
}
if err = m.LoadUsers(); err != nil {
return nil, err
}
if err = m.LoadComics(); err != nil {
if err = m.LoadFeeds(); err != nil {
return nil, err
}
@ -55,16 +58,15 @@ func (m *model) initDB() error {
if err = m.bolt.MkBucketPath([]string{"users"}); err != nil {
return err
}
if err = m.bolt.MkBucketPath([]string{"comics"}); err != nil {
if err = m.bolt.MkBucketPath([]string{"feeds"}); err != nil {
return err
}
return nil
}
func (m *model) saveChanges() {
m.Site.LastSave = time.Now()
m.SaveSite()
//m.SaveAllComics(m.Comics)
m.Site.SaveToDB()
//m.SaveAllFeeds(m.Feeds)
m.SaveAllUsers(m.Users)
}
@ -76,14 +78,14 @@ func (m *model) UpdateAllUserFeeds() {
allSubs = addStringIfUnique(sub, allSubs)
}
}
// So we have allSubs which contains all subscribed comics for all users
// So we have allSubs which contains all subscribed feeds for all users
for _, sub := range allSubs {
fmt.Println("Updating Comic: " + sub)
fmt.Println("Updating Feed: " + sub)
pts := strings.Split(sub, ";")
if len(pts) != 2 {
continue
}
c, err := m.GetComic(pts[0], pts[1])
c, err := m.GetFeed(pts[0], pts[1])
if err != nil {
fmt.Println(sub, ":", err)
continue
@ -92,7 +94,7 @@ func (m *model) UpdateAllUserFeeds() {
fmt.Println(sub, ":", err.Error())
continue
}
if err = m.SaveComic(c); err != nil {
if err = m.SaveFeed(c); err != nil {
fmt.Println(sub, ":", err.Error())
continue
}
@ -100,7 +102,7 @@ func (m *model) UpdateAllUserFeeds() {
}
type Source interface {
downloadList() []Comic
downloadList() []Feed
getRssItem(slug string) (string, error)
getUrl(slug string, dt time.Time) (string, error)
getDescription(slug string, dt time.Time) (string, error)

View File

@ -1,202 +0,0 @@
package main
import (
"errors"
"time"
)
type Comic struct {
Name string
Artist string
Slug string
Source string
Desc string
LastUpdate time.Time
}
func NewComic(s, n, a, source string) *Comic {
return &Comic{
Name: n,
Artist: a,
Slug: s,
Source: source,
}
}
func (c *Comic) GetBucket() []string {
return []string{"comics", c.Source, c.Slug}
}
func (c *Comic) Update() error {
dt := time.Now()
desc, err := getComicDesc(c.Source, c.Slug, dt)
if err != nil {
return err
}
if desc == c.Desc {
return errors.New("Comic didn't change")
}
c.Desc = desc
c.LastUpdate = dt
return nil
}
func (c *Comic) GetUrl(dt time.Time) string {
var v string
var e error
if v, e = getComicUrl(c.Source, c.Slug, dt); e != nil {
return ""
}
return v
}
func (c *Comic) GetDesc(dt time.Time) string {
var v string
var e error
if v, e = getComicDesc(c.Source, c.Slug, dt); e != nil {
return ""
}
return v
}
func (c *Comic) GetRssItem() string {
var v string
var e error
if v, e = getRssItem(c.Source, c.Slug); e != nil {
return ""
}
return v
}
// DB Function to save a comic
func (m *model) SaveComic(c *Comic) error {
var err error
if err = m.bolt.OpenDB(); err != nil {
return err
}
defer m.bolt.CloseDB()
bkt := c.GetBucket()
if err = m.bolt.MkBucketPath(bkt); err != nil {
return err
}
if err = m.bolt.SetValue(bkt, "name", c.Name); err != nil {
return err
}
if err = m.bolt.SetValue(bkt, "artist", c.Artist); err != nil {
return err
}
if err = m.bolt.SetValue(bkt, "desc", c.Desc); err != nil {
return err
}
if err = m.bolt.SetTimestamp(bkt, "lastupdate", c.LastUpdate); err != nil {
return err
}
// Add it to the cached list
for i, v := range m.Comics {
if v.Source == c.Source && v.Slug == c.Slug {
m.Comics[i] = *c
return nil
}
}
m.Comics = append(m.Comics, *c)
return nil
}
// DB Function to get a comic
func (m *model) GetComic(source, slug string) (*Comic, error) {
var err error
if err = m.bolt.OpenDB(); err != nil {
return nil, err
}
defer m.bolt.CloseDB()
ret := new(Comic)
ret.Source = source
ret.Slug = slug
bkt := ret.GetBucket()
if ret.Name, err = m.bolt.GetValue(bkt, "name"); err != nil {
return nil, err
}
if ret.Artist, err = m.bolt.GetValue(bkt, "artist"); err != nil {
return nil, err
}
if ret.Desc, err = m.bolt.GetValue(bkt, "desc"); err != nil {
return nil, err
}
if ret.LastUpdate, err = m.bolt.GetTimestamp(bkt, "lastupdate"); err != nil {
return nil, err
}
return ret, nil
}
// Load all comics into the model
func (m *model) LoadComics() error {
m.Comics = m.GetAllComics()
return nil
}
// Save all comics to the DB
func (m *model) SaveAllComics(comics []Comic) {
var err error
if err = m.bolt.OpenDB(); err != nil {
return
}
defer m.bolt.CloseDB()
for i := range comics {
m.SaveComic(&comics[i])
}
}
// Get all comics from the db
func (m *model) GetAllComics() []Comic {
var ret []Comic
var err error
if err = m.bolt.OpenDB(); err != nil {
return ret
}
defer m.bolt.CloseDB()
var srcs []string
bkt := []string{"comics"}
if srcs, err = m.bolt.GetBucketList(bkt); err != nil {
return ret
}
for _, src := range srcs {
srcBkt := append(bkt, src)
var slugs []string
if slugs, err = m.bolt.GetBucketList(srcBkt); err != nil {
return ret
}
for _, slg := range slugs {
c, err := m.GetComic(src, slg)
if err == nil {
ret = append(ret, *c)
}
}
}
return ret
}
// Delete a comic from the DB
func (m *model) DeleteComic(slug string) error {
var err error
if err = m.bolt.OpenDB(); err != nil {
return err
}
defer m.bolt.CloseDB()
return m.bolt.DeleteBucket([]string{"comics"}, slug)
}
func (m *model) RemoveMissingComics(comics []Comic) {
for _, c := range m.Comics {
var fnd bool
for _, nc := range comics {
if nc.Slug == c.Slug {
fnd = true
break
}
}
if !fnd {
m.DeleteComic(c.Slug)
}
}
}

209
model_feeds.go Normal file
View File

@ -0,0 +1,209 @@
package main
import (
"time"
)
type Feed struct {
Name string
Author string
Slug string
Source string
Desc string
LastUpdate time.Time
}
func NewFeed(s, n, a, source string) *Feed {
return &Feed{
Name: n,
Author: a,
Slug: s,
Source: source,
}
}
func (f *Feed) GetBucket() []string {
return []string{"feed", f.Source, f.Slug}
}
func (f *Feed) Update() error {
/*
dt := time.Now()
desc, err := getFeedDesc(f.Source, f.Slug, dt)
if err != nil {
return err
}
if desc == f.Desc {
return errors.New("Feed didn't change")
}
f.Desc = desc
f.LastUpdate = dt
*/
return nil
}
func (f *Feed) GetUrl(dt time.Time) string {
var v string
/*
var e error
if v, e = getFeedUrl(f.Source, f.Slug, dt); e != nil {
return ""
}
*/
return v
}
func (f *Feed) GetDesc(dt time.Time) string {
var v string
/*
var e error
if v, e = getFeedDesc(f.Source, f.Slug, dt); e != nil {
return ""
}
*/
return v
}
func (f *Feed) GetRssItem() string {
var v string
/*
var e error
if v, e = getRssItem(f.Source, f.Slug); e != nil {
return ""
}
*/
return v
}
// DB Function to save a feed
func (m *model) SaveFeed(f *Feed) error {
var err error
if err = m.bolt.OpenDB(); err != nil {
return err
}
defer m.bolt.CloseDB()
bkt := f.GetBucket()
if err = m.bolt.MkBucketPath(bkt); err != nil {
return err
}
if err = m.bolt.SetValue(bkt, "name", f.Name); err != nil {
return err
}
if err = m.bolt.SetValue(bkt, "author", f.Author); err != nil {
return err
}
if err = m.bolt.SetValue(bkt, "desc", f.Desc); err != nil {
return err
}
if err = m.bolt.SetTimestamp(bkt, "lastupdate", f.LastUpdate); err != nil {
return err
}
// Add it to the cached list
for i, v := range m.Feeds {
if v.Source == f.Source && v.Slug == f.Slug {
m.Feeds[i] = *f
return nil
}
}
m.Feeds = append(m.Feeds, *f)
return nil
}
// DB Function to get a feed
func (m *model) GetFeed(source, slug string) (*Feed, error) {
var err error
if err = m.bolt.OpenDB(); err != nil {
return nil, err
}
defer m.bolt.CloseDB()
ret := new(Feed)
ret.Source = source
ret.Slug = slug
bkt := ret.GetBucket()
if ret.Name, err = m.bolt.GetValue(bkt, "name"); err != nil {
return nil, err
}
if ret.Author, err = m.bolt.GetValue(bkt, "author"); err != nil {
return nil, err
}
if ret.Desc, err = m.bolt.GetValue(bkt, "desc"); err != nil {
return nil, err
}
if ret.LastUpdate, err = m.bolt.GetTimestamp(bkt, "lastupdate"); err != nil {
return nil, err
}
return ret, nil
}
// Load all feeds into the model
func (m *model) LoadFeeds() error {
m.Feeds = m.GetAllFeeds()
return nil
}
// Save all feeds to the DB
func (m *model) SaveAllFeeds(feeds []Feed) {
var err error
if err = m.bolt.OpenDB(); err != nil {
return
}
defer m.bolt.CloseDB()
for i := range feeds {
m.SaveFeed(&feeds[i])
}
}
// Get all feeds from the db
func (m *model) GetAllFeeds() []Feed {
var ret []Feed
var err error
if err = m.bolt.OpenDB(); err != nil {
return ret
}
defer m.bolt.CloseDB()
var srcs []string
bkt := []string{"feeds"}
if srcs, err = m.bolt.GetBucketList(bkt); err != nil {
return ret
}
for _, src := range srcs {
srcBkt := append(bkt, src)
var slugs []string
if slugs, err = m.bolt.GetBucketList(srcBkt); err != nil {
return ret
}
for _, slg := range slugs {
c, err := m.GetFeed(src, slg)
if err == nil {
ret = append(ret, *c)
}
}
}
return ret
}
// Delete a feed from the DB
func (m *model) DeleteFeed(slug string) error {
var err error
if err = m.bolt.OpenDB(); err != nil {
return err
}
defer m.bolt.CloseDB()
return m.bolt.DeleteBucket([]string{"feeds"}, slug)
}
func (m *model) RemoveMissingFeeds(feeds []Feed) {
for _, c := range m.Feeds {
var fnd bool
for _, nc := range feeds {
if nc.Slug == c.Slug {
fnd = true
break
}
}
if !fnd {
m.DeleteFeed(c.Slug)
}
}
}

View File

@ -1,86 +0,0 @@
package main
import "time"
type SiteData struct {
Title string
Port int
SessionName string
ServerDir string
SessionSecret string
LastSave time.Time
}
func NewSiteData() *SiteData {
ret := new(SiteData)
ret.Title = "BCW Comic Feed"
ret.Port = 8080
ret.SessionName = "bcw-comic-feed"
ret.ServerDir = "./"
return ret
}
func (m *model) LoadSiteData() {
m.Site = m.GetSite()
}
func (m *model) SaveSite() error {
var err error
if err = m.bolt.OpenDB(); err != nil {
return err
}
defer m.bolt.CloseDB()
bkt := []string{"site"}
if err = m.bolt.SetValue(bkt, "title", m.Site.Title); err != nil {
return err
}
if err = m.bolt.SetInt(bkt, "port", m.Site.Port); err != nil {
return err
}
if err = m.bolt.SetValue(bkt, "session-name", m.Site.SessionName); err != nil {
return err
}
if err = m.bolt.SetValue(bkt, "session-secret", m.Site.SessionSecret); err != nil {
return err
}
if err = m.bolt.SetValue(bkt, "server-dir", m.Site.ServerDir); err != nil {
return err
}
if err = m.bolt.SetTimestamp(bkt, "last-save", m.Site.LastSave); err != nil {
return err
}
return nil
}
func (m *model) GetSite() *SiteData {
s := NewSiteData()
var err error
if err = m.bolt.OpenDB(); err != nil {
return s
}
defer m.bolt.CloseDB()
bkt := []string{"site"}
var wrkStr string
var wrkInt int
var wrkTm time.Time
if wrkStr, err = m.bolt.GetValue(bkt, "title"); err == nil {
s.Title = wrkStr
}
if wrkInt, err = m.bolt.GetInt(bkt, "port"); err == nil {
s.Port = wrkInt
}
if wrkStr, err = m.bolt.GetValue(bkt, "session-name"); err == nil {
s.SessionName = wrkStr
}
if wrkStr, err = m.bolt.GetValue(bkt, "session-secret"); err == nil {
s.SessionSecret = wrkStr
}
if wrkStr, err = m.bolt.GetValue(bkt, "server-dir"); err == nil {
s.ServerDir = wrkStr
}
if wrkTm, err = m.bolt.GetTimestamp(bkt, "last-save"); err == nil {
s.LastSave = wrkTm
}
return s
}

106
model_sitedata.go Normal file
View File

@ -0,0 +1,106 @@
package main
import (
"strings"
"time"
)
/**
* SiteData
* Contains configuration for the website
*/
type SiteData struct {
Title string
Ip string
Port int
SessionName string
ServerDir string
DevMode bool
m *model
mPath []string // The path in the db to this site data
changed bool
lastSave time.Time
sessionSecret string
}
// NewSiteData returns a SiteData object with the default values
func NewSiteData(m *model) *SiteData {
ret := new(SiteData)
ret.Title = "ribbit"
ret.Ip = "127.0.0.1"
ret.Port = 8080
ret.SessionName = "good-ol-ribbit"
ret.ServerDir = "./"
ret.mPath = []string{"site"}
ret.m = m
return ret
}
// load the site data out of the database
// If fields don't exist in the DB, don't clobber what is already in s
func (s *SiteData) LoadFromDB() error {
if err := s.m.bolt.OpenDB(); err != nil {
return err
}
defer s.m.bolt.CloseDB()
if title, _ := s.m.bolt.GetValue(s.mPath, "title"); strings.TrimSpace(title) != "" {
s.Title = title
}
if ip, err := s.m.bolt.GetValue(s.mPath, "ip"); err == nil {
s.Ip = ip
}
if port, err := s.m.bolt.GetInt(s.mPath, "port"); err == nil {
s.Port = port
}
if sessionName, _ := s.m.bolt.GetValue(s.mPath, "session-name"); strings.TrimSpace(sessionName) != "" {
s.SessionName = sessionName
}
if serverDir, _ := s.m.bolt.GetValue(s.mPath, "server-dir"); strings.TrimSpace(serverDir) != "" {
s.ServerDir = serverDir
}
s.changed = false
if secret, _ := s.m.bolt.GetValue(s.mPath, "session-secret"); strings.TrimSpace(secret) != "" {
s.sessionSecret = secret
}
return nil
}
// Return if the site data in memory has changed
func (s *SiteData) NeedsSave() bool {
return s.changed
}
// Save the site data into the DB
func (s *SiteData) SaveToDB() error {
s.lastSave = time.Now()
var err error
if err = s.m.bolt.OpenDB(); err != nil {
return err
}
defer s.m.bolt.CloseDB()
if err = s.m.bolt.SetValue(s.mPath, "title", s.Title); err != nil {
return err
}
if err = s.m.bolt.SetValue(s.mPath, "ip", s.Ip); err != nil {
return err
}
if err = s.m.bolt.SetInt(s.mPath, "port", s.Port); err != nil {
return err
}
if err = s.m.bolt.SetValue(s.mPath, "session-name", s.SessionName); err != nil {
return err
}
if err = s.m.bolt.SetValue(s.mPath, "server-dir", s.ServerDir); err != nil {
return err
}
s.changed = false
if err = s.m.bolt.SetValue(s.mPath, "session-secret", s.sessionSecret); err != nil {
return err
}
return nil
}

View File

@ -6,12 +6,14 @@ import (
"strings"
"github.com/pborman/uuid"
"golang.org/x/crypto/bcrypt"
)
type User struct {
Username string `json:username`
Uuid string `json:uuid`
SubSlugs []string `json:subs`
IsAdmin bool `json:admin`
}
func NewUser(un string) *User {
@ -21,6 +23,7 @@ func NewUser(un string) *User {
return u
}
/*
func (u *User) UpdateFeed() error {
for _, slug := range u.SubSlugs {
pts := strings.Split(slug, ";")
@ -33,6 +36,7 @@ func (u *User) UpdateFeed() error {
}
return nil
}
*/
func (m *model) SaveUser(u *User) error {
var err error
@ -47,6 +51,13 @@ func (m *model) SaveUser(u *User) error {
if err = m.bolt.SetValue(bkt, "username", u.Username); err != nil {
return err
}
var adminVal = "false"
if u.IsAdmin {
adminVal = "true"
}
if err = m.bolt.SetValue(bkt, "admin", adminVal); err != nil {
return err
}
var newSubs []string
for _, v := range u.SubSlugs {
if strings.TrimSpace(v) != "" {
@ -60,7 +71,39 @@ func (m *model) SaveUser(u *User) error {
return nil
}
func (m *model) isValidUser(uid string) bool {
var err error
if err = m.bolt.OpenDB(); err != nil {
return false
}
defer m.bolt.CloseDB()
ret := new(User)
bkt := []string{"users", uid}
ret.Uuid = uid
if ret.Username, err = m.bolt.GetValue(bkt, "username"); err != nil {
fmt.Println("Error getting username value:", err.Error())
return false
}
return true
}
func (m *model) GetUserByEmail(email string) (*User, error) {
if email == "" {
return nil, errors.New("No email given")
}
u := m.GetAllUsers()
for i := range u {
if u[i].Username == email {
return &u[i], nil
}
}
return nil, errors.New("No user found")
}
func (m *model) GetUser(uid string) (*User, error) {
if uid == "" {
return nil, errors.New("No user id given")
}
var err error
if err = m.bolt.OpenDB(); err != nil {
return nil, err
@ -73,6 +116,8 @@ func (m *model) GetUser(uid string) (*User, error) {
fmt.Println("Error getting username value:", err.Error())
return nil, err
}
admin, _ := m.bolt.GetValue(bkt, "admin")
ret.IsAdmin = (admin == "true")
var subs string
if subs, err = m.bolt.GetValue(bkt, "subs"); err != nil {
return nil, err
@ -143,3 +188,50 @@ func (m *model) GetUserIdList() []string {
ret, _ = m.bolt.GetBucketList(bkt)
return ret
}
// updateUserPassword
// Takes a user id and a password
// Fails if the user doesn't exist
func (m *model) updateUserPassword(uid, password string) error {
cryptPw, cryptError := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if cryptError != nil {
return cryptError
}
if _, err := m.GetUser(uid); err != nil {
return err
}
if err := m.bolt.OpenDB(); err != nil {
return err
}
defer m.bolt.CloseDB()
usrPath := []string{"users", uid}
return m.bolt.SetValue(usrPath, "password", string(cryptPw))
}
// Is the uid and pw given valid?
func (m *model) checkCredentials(uid, pw string) error {
var err error
if err = m.bolt.OpenDB(); err != nil {
return err
}
defer m.bolt.CloseDB()
var uPw string
usrPath := []string{"users", uid}
if uPw, err = m.bolt.GetValue(usrPath, "password"); err != nil {
return err
}
return bcrypt.CompareHashAndPassword([]byte(uPw), []byte(pw))
}
func (m *model) hasAdminUser() bool {
users := m.GetAllUsers()
for i := range users {
if users[i].IsAdmin {
return true
}
}
return false
}

View File

@ -1,6 +1,7 @@
package main
import (
"errors"
"fmt"
"net/http"
@ -16,6 +17,9 @@ type pageSession struct {
}
func (p *pageSession) getStringValue(key string) (string, error) {
if p.session == nil {
return "", errors.New("Session is nil")
}
val := p.session.Values[key]
var retVal string
var ok bool

View File

@ -1,5 +1,8 @@
package main
import (
"C"
)
import (
"errors"
"fmt"
@ -9,33 +12,33 @@ import (
"github.com/PuerkitoBio/goquery"
)
func downloadDilbertList() []Comic {
var ret []Comic
ret = append(ret, *NewComic("dilbert", "Dilbert", "Scott Adams", "dilbert"))
func getFeedList() []Feed {
var ret []Feed
ret = append(ret, *NewFeed("dilbert", "Dilbert", "Scott Adams", "dilbert"))
return ret
}
func getDilbertRssItem(slug string) (string, error) {
desc, err := getDilbertFeedDesc(time.Now())
func getRssItem(slug string) (string, error) {
desc, err := getFeedDesc(time.Now())
if err != nil {
return "", err
}
comic, err := m.GetComic(SRC_DILBERT, slug)
feed, err := m.GetFeed(SRC_DILBERT, slug)
if err != nil {
return "", err
}
desc = "<![CDATA[" + desc + "]]>"
ret := " <item>\n"
ret += " <title>" + comic.Name + "</title>\n"
ret += " <pubDate>" + comic.LastUpdate.Format(time.RFC1123Z) + "</pubDate>\n"
ret += " <guid>dilbert;" + slug + ";" + comic.LastUpdate.Format(time.RFC1123Z) + "</guid>\n"
ret += " <link>" + getDilbertComicUrl(time.Now()) + "</link>\n"
ret += " <title>" + feed.Name + "</title>\n"
ret += " <pubDate>" + feed.LastUpdate.Format(time.RFC1123Z) + "</pubDate>\n"
ret += " <guid>dilbert;" + slug + ";" + feed.LastUpdate.Format(time.RFC1123Z) + "</guid>\n"
ret += " <link>" + getFeedUrl(time.Now()) + "</link>\n"
ret += " <description>" + desc + "</description>\n"
ret += " </item>\n"
return ret, nil
}
func getDilbertComicUrl(date time.Time) string {
func getFeedUrl(date time.Time) string {
return fmt.Sprintf(
"http://dilbert.com/strip/%4d-%02d-%02d",
date.Year(),
@ -44,8 +47,8 @@ func getDilbertComicUrl(date time.Time) string {
)
}
func getDilbertFeedDesc(date time.Time) (string, error) {
res, err := http.Get(getDilbertComicUrl(date))
func getFeedDesc(date time.Time) (string, error) {
res, err := http.Get(getFeedUrl(date))
if err != nil {
return "", err
}

View File

@ -1,6 +1,7 @@
package main
import (
"C"
"errors"
"fmt"
"net/http"
@ -10,8 +11,8 @@ import (
"github.com/PuerkitoBio/goquery"
)
func downloadGoComicsList() []Comic {
var ret []Comic
func downloadList() {
var ret []Feed
lstUrl := "http://www.gocomics.com/comics/a-to-z"
res, err := http.Get(lstUrl)
if err != nil {
@ -47,14 +48,14 @@ func downloadGoComicsList() []Comic {
}
author = strings.TrimPrefix(author, "By ")
author = strings.Replace(author, "\u0026", "&", -1)
ret = append(ret, *NewComic(slug, name, author, "gocomics"))
ret = append(ret, *NewFeed(slug, name, author, "gocomics"))
}
})
return ret
}
func getGoComicsRssItem(slug string) (string, error) {
desc, err := getGoComicsFeedDesc(slug, time.Now())
func getRssItem(slug string) (string, error) {
desc, err := getFeedDesc(slug, time.Now())
if err != nil {
return "", err
}
@ -73,7 +74,7 @@ func getGoComicsRssItem(slug string) (string, error) {
return ret, nil
}
func getGoComicsComicUrl(slug string, date time.Time) string {
func getFeedUrl(slug string, date time.Time) string {
return fmt.Sprintf(
"http://www.gocomics.com/%s/%04d/%02d/%02d",
slug,
@ -83,8 +84,8 @@ func getGoComicsComicUrl(slug string, date time.Time) string {
)
}
func getGoComicsFeedDesc(slug string, date time.Time) (string, error) {
res, err := http.Get(getGoComicsComicUrl(slug, date))
func getFeedDesc(slug string, date time.Time) (string, error) {
res, err := http.Get(getFeedUrl(slug, date))
if err != nil {
return "", err
}

84
plugin_src/plugin_xkcd.go Normal file
View File

@ -0,0 +1,84 @@
package main
import (
"C"
"errors"
"fmt"
"net/http"
"time"
"github.com/PuerkitoBio/goquery"
)
func getFeedList() []Feed {
var ret []Feed
ret = append(ret, *NewFeed("xkcd", "XKCD", "Randall Munroe", "xkcd"))
return ret
}
func getRssItem(slug string) (string, error) {
desc, err := getFeedDesc(time.Now())
if err != nil {
return "", err
}
feed, err := m.GetFeed(SRC_XKCD, slug)
if err != nil {
return "", err
}
desc = "<![CDATA[" + desc + "]]>"
ret := " <item>\n"
ret += " <title>" + feed.Name + "</title>\n"
ret += " <pubDate>" + feed.LastUpdate.Format(time.RFC1123Z) + "</pubDate>\n"
ret += " <guid>xkcd;" + slug + ";" + feed.LastUpdate.Format(time.RFC1123Z) + "</guid>\n"
ret += " <link>" + getUrl(time.Now()) + "</link>\n"
ret += " <description>" + desc + "</description>\n"
ret += " </item>\n"
return ret, nil
}
func getFeedUrl(date time.Time) (string, error) {
var isComicDay = func(dt time.Time) bool {
return dt.Weekday() == time.Monday || dt.Weekday() == time.Wednesday || dt.Weekday() == time.Friday
}
if !isComicDay(dt) {
return "", errors.New("No URL for the given day")
}
var num int
wrkDate := time.Date(2005, time.August, 19, 0, 0, 0, 0, time.UTC)
for wrkDate.Before(dt) {
if isComicDay(wrkDate) {
num++
}
wrkDate = wrkDate.Add(time.Hour * 24)
}
return fmt.Sprintf("https://xkcd.com/%d", num), nil
}
func getFeedDesc(date time.Time) (string, error) {
res, err := http.Get(getUrl(date))
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return "", errors.New(fmt.Sprintf("Status code error: %d %s", res.StatusCode, res.Status))
}
// Load the HTML document
doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil {
return "", err
}
// Find the Picture
sel := doc.Find("div#comic>img")
src, exists := sel.Attr("src")
if !exists {
return "", errors.New("Couldn't find image source")
}
src = "https:" + src
title, exists := sel.Attr("title")
if !exists {
title = ""
}
return "<img src=\"" + src + "\" /><p>" + title + "</p>", nil
}

11
templates/footer.html Normal file
View File

@ -0,0 +1,11 @@
</div>
<div id="modal-overlay">
<div>
<h1 id="modal-title"></h1>
<h2 id="modal-subtitle"></h2>
<div id="modal-body"></div>
<div id="modal-buttons">
</div>
<div class="reset-pull"></div>
</div>
</div>

9
templates/header.html Normal file
View File

@ -0,0 +1,9 @@
<div class="flash center {{.FlashClass}}">
{{.FlashMessage}}
</div>
<div class="content center-all">
{{ if .SubTitle }}
<div class="header-menu">
<h2>{{.SubTitle}}</h2>
</div>
{{ end }}

View File

@ -0,0 +1,6 @@
</div>
{{ range $i, $v := .Scripts }}
<script src="{{ $v }}"></script>
{{ end }}
</body>
</html>

25
templates/htmlheader.html Normal file
View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta http-equiv="Cache-control" content="No-Cache">
<link rel="apple-touch-icon" href="/assets/img/favicon.png" type="image/png">
<link rel="shortcut icon" href="/assets/img/favicon.png" type="image/png">
<title>{{.Site.Title}} - {{.SubTitle}}</title>
<!--[if lt IE 9]>
<script src="http://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7/html5shiv.js"></script>
<![endif]-->
{{ range $i, $v := .Stylesheets }}
<link rel="stylesheet" href="{{ $v }}">
{{ end }}
{{ range $i, $v := .HeaderScripts }}
<script src="{{ $v }}"></script>
{{ end }}
</head>
<body>
<div id="layout">

22
templates/login.html Normal file
View File

@ -0,0 +1,22 @@
<div class="half center-all">
<form class="pure-form pure-form-aligned" action="/user/login" method="POST">
<fieldset>
<div class="pure-control-group">
<label for="email">Email Address</label>
<input id="email" name="email" type="text" placeholder="Email Address" autofocus>
</div>
<div class="pure-control-group">
<label for="password">Password</label>
<input id="password" name="password" type="password" placeholder="Password">
</div>
<div class="pure-controls">
<label for="remember" class="pure-checkbox">
<input id="remember" name="remember" type="checkbox"> Remember Me
</label>
</div>
<button type="submit" class="pure-button pure-button-primary">Submit</button>
</fieldset>
</form>
</div>

View File