initial commit

This commit is contained in:
mappu 2017-07-09 11:13:36 +12:00
commit 7cb1f02423
7 changed files with 668 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Binaries
cmd/yatwiki-server/yatwiki-server
# Development db files
cmd/yatwiki-server/wiki.db

146
DB.go Normal file
View File

@ -0,0 +1,146 @@
package yatwiki3
import (
"crypto/md5"
"database/sql"
"encoding/hex"
"fmt"
"net/http"
"time"
_ "github.com/mattn/go-sqlite3"
)
type WikiDB struct {
db *sql.DB
}
func NewWikiDB(dbFilePath string) (*WikiDB, error) {
db, err := sql.Open("sqlite3", dbFilePath)
if err != nil {
return nil, err
}
wdb := WikiDB{db: db}
err = wdb.assertSchema()
if err != nil {
return nil, fmt.Errorf("assertSchema: %s", err.Error())
}
return &wdb, nil
}
func (this *WikiDB) assertSchema() error {
_, err := this.db.Exec(`
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY,
article INTEGER,
modified INTEGER,
body BLOB,
author TEXT
);`)
if err != nil {
return err
}
_, err = this.db.Exec(`
CREATE TABLE IF NOT EXISTS titles (
id INTEGER PRIMARY KEY,
title TEXT
);`)
if err != nil {
return err
}
_, err = this.db.Exec(`
CREATE INDEX IF NOT EXISTS articles_modified_index ON articles (modified)
`)
if err != nil {
return err
}
_, err = this.db.Exec(`
CREATE INDEX IF NOT EXISTS articles_title_index ON articles (article)
`)
if err != nil {
return err
}
return nil
}
type Article struct {
ID int
TitleID int
Modified int64
Body []byte
Author string
}
func (this *Article) FillModifiedTimestamp() {
this.Modified = time.Now().Unix()
}
func (this *Article) FillAuthor(r *http.Request) {
userAgentHash := md5.Sum([]byte(r.UserAgent()))
this.Author = r.RemoteAddr + "-" + hex.EncodeToString(userAgentHash[:])[:6]
}
type ArticleWithTitle struct {
Article
Title string
}
func (this *WikiDB) GetArticleById(articleId int) (*Article, error) {
row := this.db.QueryRow(`SELECT articles.* FROM articles WHERE id = ?`, articleId)
return this.parseArticle(row)
}
func (this *WikiDB) GetLatestVersion(title string) (*Article, error) {
row := this.db.QueryRow(`SELECT articles.* FROM articles WHERE article = (SELECT id FROM titles WHERE title = ?) ORDER BY modified DESC LIMIT 1`, title)
return this.parseArticle(row)
}
func (this *WikiDB) ListTitles() ([]string, error) {
rows, err := this.db.Query(`SELECT title FROM titles ORDER BY title ASC`)
if err != nil {
return nil, err
}
defer rows.Close()
ret := make([]string, 0)
for rows.Next() {
var title string
err = rows.Scan(&title)
if err != nil {
return nil, err
}
ret = append(ret, title)
}
return ret, nil
}
func (this *WikiDB) parseArticle(row *sql.Row) (*Article, error) {
a := Article{}
err := row.Scan(&a.ID, &a.TitleID, &a.Modified, &a.Body, &a.Author)
if err != nil {
return nil, err
}
return &a, nil
}
func (this *WikiDB) parseArticleWithTitle(row *sql.Row) (*ArticleWithTitle, error) {
a := ArticleWithTitle{}
err := row.Scan(&a.ID, &a.TitleID, &a.Modified, &a.Body, &a.Author, &a.Title)
if err != nil {
return nil, err
}
return &a, nil
}
func (this *WikiDB) Close() {
this.db.Close()
}

31
ServerOptions.go Normal file
View File

@ -0,0 +1,31 @@
package yatwiki3
import (
"time"
)
type ServerOptions struct {
PageTitle string
ExpectBaseURL string
DefaultPage string
Timezone string
DateFormat string
DBFilePath string
AllowDBDownload bool
RecentChanges int
GzipCompressionLevel int
}
func DefaultOptions() *ServerOptions {
return &ServerOptions{
PageTitle: "YATWiki",
ExpectBaseURL: "/",
DefaultPage: "home",
Timezone: "UTC",
DateFormat: time.RFC822Z,
DBFilePath: "wiki.db",
AllowDBDownload: true,
RecentChanges: 20,
GzipCompressionLevel: 9,
}
}

59
WikiServer.go Normal file
View File

@ -0,0 +1,59 @@
package yatwiki3
import (
"html/template"
"log"
"net/http"
)
type WikiServer struct {
db *WikiDB
opts *ServerOptions
pageTmp *template.Template
}
func NewWikiServer(opts *ServerOptions) (*WikiServer, error) {
wdb, err := NewWikiDB(opts.DBFilePath)
if err != nil {
return nil, err
}
tmpl, err := template.New("yatwiki/page").Parse(pageTemplate)
if err != nil {
panic(err)
}
ws := WikiServer{
db: wdb,
opts: opts,
pageTmp: tmpl,
}
return &ws, nil
}
func (this *WikiServer) Close() {
this.db.Close()
}
func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "Yatwiki3")
if r.URL.Path == this.opts.ExpectBaseURL+"wiki.css" {
w.Header().Set("Content-Type", "text/css")
w.Write(tmplWikiCss)
return
}
pto := DefaultPageTemplateOptions(this.opts)
pto.SessionMessage = `Invalid request.`
//pto.CurrentPageIsArticle = true
//pto.CurrentPageName = "quotes/\"2017"
this.servePageResponse(w, pto)
}
func (this *WikiServer) servePageResponse(w http.ResponseWriter, pto *pageTemplateOptions) {
err := this.pageTmp.Execute(w, pto)
if err != nil {
log.Println(err.Error())
}
}

View File

@ -0,0 +1,35 @@
package main
import (
"flag"
"fmt"
"net/http"
"os"
"code.ivysaur.me/yatwiki3"
)
func main() {
bindAddr := flag.String("listen", "127.0.0.1:80", "Bind address")
dbPath := flag.String("database", "wiki.db", "Database file")
flag.Parse()
opts := yatwiki3.DefaultOptions()
opts.DBFilePath = *dbPath
ws, err := yatwiki3.NewWikiServer(opts)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
defer ws.Close()
err = http.ListenAndServe(*bindAddr, ws)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
os.Exit(0)
}

113
pageTemplate.go Normal file
View File

@ -0,0 +1,113 @@
package yatwiki3
import (
"fmt"
"html/template"
"time"
)
var subresourceNonce = time.Now().Unix()
type pageTemplateOptions struct {
CurrentPageIsArticle bool
CurrentPageName string
WikiTitle string
Content template.HTML
BaseURL string
LoadCodeResources bool
DefaultPage string
AllowDownload bool
SessionMessage template.HTML
}
func DefaultPageTemplateOptions(opts *ServerOptions) *pageTemplateOptions {
return &pageTemplateOptions{
WikiTitle: opts.PageTitle,
BaseURL: opts.ExpectBaseURL,
DefaultPage: opts.DefaultPage,
AllowDownload: opts.AllowDBDownload,
}
}
func (this *pageTemplateOptions) NewArticleTitle() string {
return fmt.Sprintf("untitled-%d", time.Now().Unix())
}
func (this *pageTemplateOptions) SubresourceNonce() int64 {
return subresourceNonce
}
const pageTemplate string = `<!DOCTYPE html>
<html>
<head>
<title>{{.CurrentPageName}}{{ if len .CurrentPageName }} - {{end}}{{.WikiTitle}}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="alternate" type="application/rss+xml" href="{{.BaseURL}}rss/changes" title="{{.WikiTitle}} - Recent Changes">
<link rel="stylesheet" type="text/css" href="{{.BaseURL}}wiki.css?_={{.SubresourceNonce}}">
{{if .LoadCodeResources}}
<script type="text/javascript" src="{{.BaseURL}}highlight.js?_={{.SubresourceNonce}}"></script>
<script type="text/javascript">hljs.tabReplace = ' ';hljs.initHighlightingOnLoad();</script>
{{end}}
<script type="text/javascript">
var a = document;
function tid(id) {
t(a.getElementById(id));
}
function ts(e) {
t(e.nextSibling);
}
function t(e) {
e.style.display=(e.style.display=='none') ? 'block' : 'none';
}
function els(e,s){ // no js exec in innerHTML
var p = e.parentNode;
p.className = "";
p.innerHTML = s;
var n = "script";
var z = p.childNodes,
m = "text/javascript",
l = function(s){ return s.toLowerCase(); };
for (var i=0, e=0; e = z[i]; i++) {
if (e.nodeName && (l(e.nodeName)===n) && (!e.type||l(e.type)===m)) {
var t = p.removeChild(e),
d = a.getElementsByTagName("head")[0],
se = a.createElement(n);
se.type = m;
se.appendChild(a.createTextNode((t.text||t.textContent||t.innerHTML||"")));
d.insertBefore(se,d.firstChild );
}
}
}
</script>
</head>
<body>
<div class="header">
<a href="{{.BaseURL}}view/{{.DefaultPage}}" title="Home"><div class="sprite hm"></div></a>
<a href="javascript:;" onclick="tid('spm');tid('tr1');tid('tr2');" title="Menu"><div class="sprite sp"></div></a>
<a href="{{.BaseURL}}modify/{{.NewArticleTitle}}" title="New Page"><div class="sprite nw"></div></a>
{{if .CurrentPageIsArticle }}
<div class="sep"></div>
<a href="{{.BaseURL}}history/{{.CurrentPageName}}" title="Page History"><div class="sprite hs"></div></a>
<a href="{{.BaseURL}}modify/{{.CurrentPageName}}" title="Modify Page"><div class="sprite ed"></div></a>
{{end}}
</div>
<div id="tr1" style="display:none;"></div>
<div id="tr2" style="display:none;"></div>
<div class="ddmenu" id="spm" style="display:none;">
<a href="{{.BaseURL}}recent"><div class="sprite no"></div> Recent Changes</a>
<a href="{{.BaseURL}}random"><div class="sprite rn"></div> Random Page</a>
<a href="{{.BaseURL}}index"><div class="sprite no"></div> Article Index</a>
{{if .AllowDownload}}
<a href="{{.BaseURL}}download-database"><div class="sprite no"></div> Download DB backup</a>
{{end}}
</div>
<div class="content">
{{if len .SessionMessage}}
<div class="info">{{.SessionMessage}}</div>
{{end}}
{{.Content}}
</div>
</body>
</html>`

279
tmpl_WikiCss.go Normal file
View File

@ -0,0 +1,279 @@
package yatwiki3
var tmplWikiCss []byte = []byte(`
/* wiki.css */
html,body {
background:white;color:black;font-size:12px;
margin:0;padding:0;border:0;
}
html,body,input{font-family:Verdana,Arial;}
table,tr,td,th {border:0px;}
input {font-size:8px;}
a {text-decoration:none;color: blue;}
a:hover {text-decoration:underline;}
img {border:0px;max-width:100%;}
ul {margin:0px; padding-left:30px;}
h2 {display:inline;}
pre {font-family:Consolas,Courier,monospace;font-size:11px;}
td {padding:0px 10px;}
.content{padding:8px;}
.s {text-decoration:line-through;}
.spoiler{color:black;background-color:black;}
.spoiler:hover{color:white;}
.imgur {
border:1px solid white;
width:90px;
height:90px;
opacity:0.6;
-moz-transition:all 0.1s linear;
-webkit-transition:all 0.1s linear;
}
.imgur:hover {opacity: 1.0;}
/* Header */
.header {
background:#DDD;
padding:3px;
font-size:0px;
box-shadow: 0px 4px 24px #CCC;
/*position:absolute;
left:0;right:0;*/
}
.header a {
background:#DDD;
color:grey;
text-decoration: none;
margin:0px 2px;
border:1px solid #DDD;
display:inline-block;
height:16px;
padding:2px 3px;
-moz-transition:all 0.1s linear;
-webkit-transition:all 0.1s linear;
}
.header a:hover {
background:white;
color:black;
border:1px solid black;
border-color:grey black black grey;
}
.info {
display:block;
border:1px solid darkgrey;
padding:2px 4px;
margin:10px 5px;
color:black;
background-color:lightyellow;
}
/* Editor page */
fieldset {border:1px solid grey;}
fieldset legend {
padding:2px 6px 2px 6px;
background:#DDD;
border:1px solid grey;
font-weight:bold;
}
.editor {
padding:0px;
margin:0px;
vertical-align:top;
}
#contentctr {
border:1px dashed lightgrey;
position:absolute;
top:120px;right:10px;bottom:10px;left:10px;
}
.editor textarea {
font-family:Consolas,Courier,monospace;font-size:10px;
margin:0;padding:0;border:0;
width:100%;height:100%;
min-width:100%;min-height:100%; /* no resize */
max-width:100%;max-height:100%;
position:absolute; /* IE7 */
}
.frm {
border:1px solid #DDD;
background:#EEE;
font-size:10px;
padding:5px;
margin:5px 10px 10px 15px;
}
/* Tables in content */
.ti {border-collapse: collapse; border-style:hidden;}
.ti td {border-left:1px solid #DDD;border-right:1px solid #DDD;}
.ti tr:first-child {font-weight:bold;}
.ti tr:first-child td {border-bottom:1px solid #DDD;}
.ti tr:hover {background-color:#F8F8F8;}
.ti tr:first-child:hover{background-color:white;}
/* Sprites */
.sprite {
display:inline-block;
width:16px;height:16px;
vertical-align:text-bottom;
background-repeat:no-repeat;
background-image: url(data:image/png;base64,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);
}
.sprite.hm { background-position:0px 0px;}
.sprite.hs { background-position:0px -16px;}
.sprite.sp { background-position:0px -32px;}
.sprite.nw { background-position:-16px 0px;}
.sprite.ed { background-position:-16px -16px;}
.sprite.rn { background-position:-16px -32px;}
.sprite.no {
background:none;
}
.sep {
display:inline-block;
*display:inline; /* IE7 */
zoom: 1; /* IE7 */
height:16px;
width:0px;
border-left:1px solid darkgrey;
border-right:1px solid #DDD;
margin:0px 4px;
}
/* Sections */
.section {
display:inline-block;
background:#F8F8F8;
width:auto;
padding:3px;
border:1px dashed #DDD;
}
.sectionheader {
color:green;
font-weight:bold;
}
/* Dropdown */
.ddmenu {
display:block;
position:absolute;
top:28px;
left:32px;
width:180px;
border-top:1px solid #999;
box-shadow: 0px 4px 24px #CCC;
z-index: 2;
}
.ddmenu a {
color:black;
display:inline-block;
border:1px solid #999;
border-top:0px;
width:170px;
padding:4px;
background:#EEE;
}
.ddmenu a:hover {
text-decoration:none;background:#FFF;
-moz-transition:all 0.1s linear;
-webkit-transition:all 0.1s linear;
}
#tr1 {
width:0px;height:0px;border-bottom:10px solid #999;
border-right:10px solid transparent;border-left:10px solid transparent;
position:absolute;top:18px;left:35px;z-index:1;
}
#tr2 {
width:0px;height:0px;border-bottom:10px solid #EEE;
border-right:10px solid transparent;border-left:10px solid transparent;
position:absolute;top:19px;left:35px;z-index:3;
}
/* Diffs, rawhtml */
del{text-decoration:none;background:red;font-weight:bold;}
ins{text-decoration:none;background:lightgreen;font-weight:bold;}
.html a {color:red;font-weight:bold;}
/*
highlight.css
Visual Studio-like style based on original C# coloring by Jason Diamond <jason@diamond.name>
*/
pre code {
display: block; padding: 0.5em;
}
pre .comment,
pre .annotation,
pre .template_comment,
pre .diff .header,
pre .chunk,
pre .apache .cbracket {
color: rgb(0, 128, 0);
}
pre .keyword,
pre .id,
pre .built_in,
pre .smalltalk .class,
pre .winutils,
pre .bash .variable,
pre .tex .command,
pre .request,
pre .status,
pre .nginx .title,
pre .xml .tag,
pre .xml .tag .value {
color: rgb(0, 0, 255);
}
pre .string,
pre .title,
pre .parent,
pre .tag .value,
pre .rules .value,
pre .rules .value .number,
pre .ruby .symbol,
pre .ruby .symbol .string,
pre .aggregate,
pre .template_tag,
pre .django .variable,
pre .addition,
pre .flow,
pre .stream,
pre .apache .tag,
pre .date,
pre .tex .formula {
color: rgb(163, 21, 21);
}
pre .ruby .string,
pre .decorator,
pre .filter .argument,
pre .localvars,
pre .array,
pre .attr_selector,
pre .pseudo,
pre .pi,
pre .doctype,
pre .deletion,
pre .envvar,
pre .shebang,
pre .preprocessor,
pre .userType,
pre .apache .sqbracket,
pre .nginx .built_in,
pre .tex .special,
pre .prompt {
color: rgb(43, 145, 175);
}
pre .phpdoc,
pre .javadoc,
pre .xmlDocTag {
color: rgb(128, 128, 128);
}
pre .vhdl .typename { font-weight: bold; }
pre .vhdl .string { color: #666666; }
pre .vhdl .literal { color: rgb(163, 21, 21); }
pre .vhdl .attribute { color: #00B0E8; }
pre .xml .attribute { color: rgb(255, 0, 0); }
`)