working save action; int64 conversion; strip port from remoteaddrs

This commit is contained in:
mappu 2017-07-09 18:45:28 +12:00
parent 92bcb013d2
commit 9b9d9542b9
5 changed files with 151 additions and 39 deletions

15
AuthorHash.go Normal file
View File

@ -0,0 +1,15 @@
package yatwiki3
import (
"crypto/md5"
"encoding/hex"
"net/http"
"strings"
)
func Author(r *http.Request) string {
userAgentHash := md5.Sum([]byte(r.UserAgent()))
ipAddr := strings.TrimRight(strings.TrimRight(r.RemoteAddr, `0123456789`), `:`) // trim trailing port; IPv4 and IPv6-safe
return ipAddr + "-" + hex.EncodeToString(userAgentHash[:])[:6]
}

100
DB.go
View File

@ -1,30 +1,29 @@
package yatwiki3 package yatwiki3
import ( import (
"bytes"
"compress/flate"
"crypto/md5"
"database/sql" "database/sql"
"encoding/hex"
"fmt" "fmt"
"io"
"net/http"
"time" "time"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
type WikiDB struct { type WikiDB struct {
db *sql.DB db *sql.DB
compressionLevel int
} }
func NewWikiDB(dbFilePath string) (*WikiDB, error) { func NewWikiDB(dbFilePath string, compressionLevel int) (*WikiDB, error) {
db, err := sql.Open("sqlite3", dbFilePath) db, err := sql.Open("sqlite3", dbFilePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
wdb := WikiDB{db: db} wdb := WikiDB{
db: db,
compressionLevel: compressionLevel,
}
err = wdb.assertSchema() err = wdb.assertSchema()
if err != nil { if err != nil {
return nil, fmt.Errorf("assertSchema: %s", err.Error()) return nil, fmt.Errorf("assertSchema: %s", err.Error())
@ -73,23 +72,14 @@ func (this *WikiDB) assertSchema() error {
} }
type Article struct { type Article struct {
ID int ID int64
TitleID int TitleID int64
Modified int64 Modified int64
Body []byte Body []byte
Author string Author string
Title string Title 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]
}
func (this *WikiDB) GetArticleById(articleId int) (*Article, error) { func (this *WikiDB) GetArticleById(articleId int) (*Article, error) {
row := this.db.QueryRow(`SELECT articles.* FROM articles WHERE id = ?`, articleId) row := this.db.QueryRow(`SELECT articles.* FROM articles WHERE id = ?`, articleId)
return this.parseArticle(row) return this.parseArticle(row)
@ -105,6 +95,57 @@ func (this *WikiDB) GetLatestVersion(title string) (*Article, error) {
return this.parseArticle(row) return this.parseArticle(row)
} }
type ArticleAlteredError struct {
got, expected int64
}
func (aae ArticleAlteredError) Error() string {
return fmt.Sprintf("Warning: Your changes were not based on the most recent version of the page (r%d ≠ r%d). No changes were saved.", aae.got, aae.expected)
}
func (this *WikiDB) SaveArticle(title, author, body string, expectBaseRev int64) error {
isNewArticle := false
a, err := this.GetLatestVersion(title)
if err != nil {
if err == sql.ErrNoRows {
isNewArticle = true
} else {
return fmt.Errorf("Couldn't check for existing article title: %s", err.Error())
}
}
if !isNewArticle && a.ID != expectBaseRev {
return ArticleAlteredError{got: expectBaseRev, expected: a.ID}
}
zBody, err := gzdeflate([]byte(body), this.compressionLevel)
if err != nil {
return err
}
var titleId int64
if isNewArticle {
titleInsert, err := this.db.Exec(`INSERT INTO titles (title) VALUES (?)`, title)
if err != nil {
return err
}
titleId, err = titleInsert.LastInsertId()
if err != nil {
return err
}
} else {
titleId = a.TitleID
}
_, err = this.db.Exec(`INSERT INTO articles (article, modified, body, author) VALUES (?, ?, ?, ?)`, titleId, time.Now().Unix(), zBody, author)
if err != nil {
return err
}
return nil
}
func (this *WikiDB) GetRevisionHistory(title string) ([]Article, error) { func (this *WikiDB) GetRevisionHistory(title string) ([]Article, error) {
rows, err := this.db.Query(`SELECT articles.id, articles.modified, articles.author FROM articles WHERE article = (SELECT id FROM titles WHERE title = ?) ORDER BY modified DESC`, title) rows, err := this.db.Query(`SELECT articles.id, articles.modified, articles.author FROM articles WHERE article = (SELECT id FROM titles WHERE title = ?) ORDER BY modified DESC`, title)
if err != nil { if err != nil {
@ -177,21 +218,6 @@ func (this *WikiDB) ListTitles() ([]string, error) {
return ret, nil return ret, nil
} }
func (this *WikiDB) gzinflate(gzBody []byte) ([]byte, error) {
gzBodyReader := bytes.NewReader(gzBody)
gzReader := flate.NewReader(gzBodyReader)
defer gzReader.Close()
buffer := bytes.Buffer{}
_, err := io.Copy(&buffer, gzReader)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (this *WikiDB) parseArticle(row *sql.Row) (*Article, error) { func (this *WikiDB) parseArticle(row *sql.Row) (*Article, error) {
a := Article{} a := Article{}
var gzBody []byte var gzBody []byte
@ -200,7 +226,7 @@ func (this *WikiDB) parseArticle(row *sql.Row) (*Article, error) {
return nil, err return nil, err
} }
decompressed, err := this.gzinflate(gzBody) decompressed, err := gzinflate(gzBody)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -218,7 +244,7 @@ func (this *WikiDB) parseArticleWithTitle(row *sql.Row) (*Article, error) {
return nil, err return nil, err
} }
decompressed, err := this.gzinflate(gzBody) decompressed, err := gzinflate(gzBody)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -19,7 +19,7 @@ type WikiServer struct {
} }
func NewWikiServer(opts *ServerOptions) (*WikiServer, error) { func NewWikiServer(opts *ServerOptions) (*WikiServer, error) {
wdb, err := NewWikiDB(opts.DBFilePath) wdb, err := NewWikiDB(opts.DBFilePath, opts.GzipCompressionLevel)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -141,6 +141,33 @@ func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} else if r.Method == "POST" {
if r.URL.Path == this.opts.ExpectBaseURL+"save" {
err := r.ParseForm()
if err != nil {
this.serveErrorMessage(w, err)
return
}
title := r.Form.Get("pname")
body := r.Form.Get("content")
expectRev, err := strconv.Atoi(r.Form.Get("baserev"))
if err != nil {
this.serveErrorMessage(w, err)
return
}
err = this.db.SaveArticle(title, Author(r), body, int64(expectRev))
if err != nil {
this.serveErrorMessage(w, err)
return
}
this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.QueryEscape(title))
return
}
} }
// No match? Add 'Page not found' to next session response, and redirect to homepage // No match? Add 'Page not found' to next session response, and redirect to homepage

44
gzflate.go Normal file
View File

@ -0,0 +1,44 @@
package yatwiki3
import (
"bytes"
"compress/flate"
"io"
)
func gzinflate(gzBody []byte) ([]byte, error) {
gzBodyReader := bytes.NewReader(gzBody)
gzReader := flate.NewReader(gzBodyReader)
defer gzReader.Close()
buffer := bytes.Buffer{}
_, err := io.Copy(&buffer, gzReader)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func gzdeflate(plaintext []byte, level int) ([]byte, error) {
compressedContent := bytes.Buffer{}
zipper, err := flate.NewWriter(&compressedContent, level)
if err != nil {
return nil, err // e.g. bad level
}
defer zipper.Close()
_, err = zipper.Write(plaintext)
if err != nil {
return nil, err
}
err = zipper.Close() // flush data
if err != nil {
return nil, err
}
return compressedContent.Bytes(), nil
}

View File

@ -26,7 +26,7 @@ func (this *WikiServer) routeModify(w http.ResponseWriter, r *http.Request, arti
} }
var pageTitleHTML string var pageTitleHTML string
var baseRev int var baseRev int64
var existingBody string var existingBody string
if isNewArticle { if isNewArticle {
pageTitleHTML = `Creating new article` pageTitleHTML = `Creating new article`