64 Commits

Author SHA1 Message Date
7b268108da makefile: set version to 3.3.0 2018-04-02 18:13:58 +12:00
4f12dd0564 doc: update README 2018-04-02 18:12:01 +12:00
0fb52800f7 bbcode: properly unhesc content before applying url escaping 2018-04-02 18:10:51 +12:00
b080a6f017 add link to raw source page when viewing a specific revision 2018-04-02 17:54:24 +12:00
ec116a09b8 deletions: hide 'view latest' link on deleted rev links 2018-04-02 17:45:55 +12:00
5148c59944 deletions: recents: mark deleted pages with css 2018-04-02 17:42:49 +12:00
0a332642d7 deletions: index page: hide deleted pages until you click a button 2018-04-02 17:42:34 +12:00
6e63285a54 db: plumbing to handle deletions, restructure Article/Title structs 2018-04-02 17:41:47 +12:00
2c5f1e9244 css: add class for deleted page links 2018-04-02 17:39:51 +12:00
6a56a4b1b2 deletions: easy delete button on page editor 2018-04-02 17:39:34 +12:00
6c70f37ef8 deletions: put [delete] as page content to block /view/ route 2018-04-02 17:08:54 +12:00
eeb2308c54 html: prevent zooming OUT (still allowed to zoom in) 2018-04-02 17:01:37 +12:00
96b5318eca db: schema upgrade 1: add titles.is_deleted column 2018-04-02 16:53:55 +12:00
9aa80bf772 cmd: simplify default-config-file logic 2018-04-02 16:53:36 +12:00
adbe71525a cmd: convert from fmt to log package 2018-04-02 16:51:08 +12:00
65e43df8d2 vendor: update go-sqlite 1.3.0->1.6.0 2018-04-02 16:48:48 +12:00
ee4ed51530 bbcode: separate string/func transformations into an interface 2018-04-02 16:46:22 +12:00
91887bcee5 db: add schema table, initial schema-upgrade system 2018-04-02 16:19:28 +12:00
993bd6c4f3 bbcode: add basic table tests 2018-04-02 15:57:41 +12:00
e0e30372ef add [youtube] tag support 2018-04-02 15:57:24 +12:00
6b41df964b bump all versions to 3.2.1 2017-11-18 15:40:08 +13:00
db659236bf doc: update readme 2017-11-18 15:37:13 +13:00
817b1690e7 bump all versions to 3.2.0 2017-11-18 15:33:35 +13:00
5b533f7b40 redesign the 'recent changes' page, include diff links, "new" display, hover rows 2017-11-18 15:30:50 +13:00
51aae382b7 hover all timestamp displays to get more detail 2017-11-18 15:30:29 +13:00
cfc0107bef staticResources.go: rebuild 2017-11-18 15:05:12 +13:00
e997e1b08a replace menu icons with inline SVG from materialdesignicons.com 2017-11-18 15:04:39 +13:00
c830c2b4dd add new ContentedBBCodeTag option for contented 1.2.0++ thumbnails 2017-11-18 15:04:19 +13:00
fdb854e6c7 doc: changelog 2017-10-29 15:10:35 +13:00
f934c2917f catch one more case of title normalisation 2017-10-29 15:09:53 +13:00
1bfefdccb3 bump all versions to 3.1.3 2017-10-29 14:11:31 +13:00
9ca58bc16c doc: update readme 2017-10-29 14:08:10 +13:00
122acf6999 rebuild staticResources.go 2017-10-29 14:05:20 +13:00
f627946c0d use 'dep' for dependency management 2017-10-29 14:04:26 +13:00
5b42685956 diff/test: fix package import path 2017-10-29 14:01:16 +13:00
90fedf86d9 serve proper 404 if favicon.ico not configured 2017-10-29 13:40:15 +13:00
a9a6b51a3f show yatwiki version in Server header 2017-10-29 13:40:00 +13:00
9687f90cf5 build: use simpler cleanup target 2017-10-29 13:29:12 +13:00
5cc93387e7 fix a regression with not normalising titles to lowercase/trim 2017-10-29 13:19:24 +13:00
fc57e4d8f3 bump version to 3.1.2 2017-10-15 20:02:20 +13:00
2bc26c5966 bump version to 3.1.1 2017-10-15 20:01:59 +13:00
f5767db840 load contented without jquery, but it's present by the callback 2017-10-15 20:00:04 +13:00
edf88d1f31 doc: changelog update 2017-10-15 19:58:04 +13:00
262c3ba903 contented: update integration to 1.1.0 2017-10-15 19:56:03 +13:00
a260d102ee doc: update readme, bump version to 3.1.0 2017-10-08 17:09:15 +13:00
179617d058 contented integration 2017-10-08 17:08:26 +13:00
5347efb51a bump version to 3.0.3 2017-08-13 18:29:21 +12:00
e3cee5b94c doc: changelog update 2017-08-13 18:27:04 +12:00
e4cf02cde7 restructure error handling to prevent reflected XSS 2017-08-13 18:25:58 +12:00
06e5b4ddf9 bump version to 3.0.1 2017-08-13 18:07:29 +12:00
dbf5e1b246 gitignore 2017-08-13 18:05:34 +12:00
fbad854279 doc: preliminary changelog update 2017-08-13 18:05:28 +12:00
d78429129f also use path escaping function for whole-template links 2017-08-13 18:04:26 +12:00
d937ea6562 prevent viewing history for unknown articles 2017-08-13 17:58:01 +12:00
884acd3040 remove debugging code 2017-08-13 17:53:21 +12:00
e515d73052 use PathEscape instead of QueryEscape for titles in URLs 2017-08-13 17:51:44 +12:00
c87eaa637a doc: preliminary changelog update 2017-08-13 17:33:27 +12:00
043720a086 new TrustXForwardedFor option 2017-08-13 17:32:54 +12:00
a12af6967c fix crash with [html] tags 2017-08-13 17:28:44 +12:00
9792c262a2 readme: fix version tags for download alignment on website 2017-08-13 13:52:40 +12:00
6d9079e1dc doc: update git repo in docs 2017-07-12 18:46:49 +12:00
1c4505a2d9 rename package yatwiki3->yatwiki 2017-07-12 18:43:11 +12:00
11a4f97212 bump all versions to 3.0.1 2017-07-12 18:41:39 +12:00
74b3124997 doc: add 'go get' information to readme 2017-07-11 20:23:17 +12:00
29 changed files with 655 additions and 225 deletions

5
.gitignore vendored
View File

@@ -6,5 +6,8 @@ build/
cmd/yatwiki-server/yatwiki-server cmd/yatwiki-server/yatwiki-server
# Development db files # Development db files
cmd/yatwiki-server/wiki.db cmd/yatwiki-server/*.db
cmd/yatwiki-server/config.json cmd/yatwiki-server/config.json
# Vendor
vendor/

View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"crypto/md5" "crypto/md5"
@@ -11,8 +11,16 @@ func RemoteAddrToIPAddress(remoteAddr string) string {
return strings.TrimRight(strings.TrimRight(remoteAddr, `0123456789`), `:`) // trim trailing port; IPv4 and IPv6-safe return strings.TrimRight(strings.TrimRight(remoteAddr, `0123456789`), `:`) // trim trailing port; IPv4 and IPv6-safe
} }
func Author(r *http.Request) string { func Author(r *http.Request, trustXForwardedFor bool) string {
userAgentHash := md5.Sum([]byte(r.UserAgent())) userAgentHash := md5.Sum([]byte(r.UserAgent()))
return RemoteAddrToIPAddress(r.RemoteAddr) + "-" + hex.EncodeToString(userAgentHash[:])[:6] ipAddress := RemoteAddrToIPAddress(r.RemoteAddr)
if trustXForwardedFor {
if xff := r.Header.Get("X-Forwarded-For"); len(xff) > 0 {
ipAddress = xff
}
}
return ipAddress + "-" + hex.EncodeToString(userAgentHash[:])[:6]
} }

163
DB.go
View File

@@ -1,8 +1,10 @@
package yatwiki3 package yatwiki
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"log"
"strings"
"time" "time"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
@@ -32,52 +34,101 @@ func NewWikiDB(dbFilePath string, compressionLevel int) (*WikiDB, error) {
return &wdb, nil return &wdb, nil
} }
func (this *WikiDB) multiTx(stmts ...string) (err error) {
tx, err := this.db.Begin()
if err != nil {
return err
}
for _, stmt := range stmts {
_, err := tx.Exec(stmt)
if err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}
func (this *WikiDB) assertSchema() error { func (this *WikiDB) assertSchema() error {
_, err := this.db.Exec(` _, err := this.db.Exec(`
CREATE TABLE IF NOT EXISTS articles ( CREATE TABLE IF NOT EXISTS schema (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY
article INTEGER,
modified INTEGER,
body BLOB,
author TEXT
);`) );`)
if err != nil { if err != nil {
return err return err
} }
_, err = this.db.Exec(` // Look up current value from schema table
CREATE TABLE IF NOT EXISTS titles ( schemaLookup := this.db.QueryRow(`SELECT MAX(id) mid FROM schema`)
id INTEGER PRIMARY KEY, currentSchema := int64(0)
title TEXT if err = schemaLookup.Scan(&currentSchema); err == nil {
);`) // That's fine
if err != nil {
return err
} }
_, err = this.db.Exec(` log.Printf("Found DB version %d\n", currentSchema)
CREATE INDEX IF NOT EXISTS articles_modified_index ON articles (modified)
`) //
if err != nil {
return err if currentSchema == 0 {
// Schema 0 ==> Schema 1
log.Println("Upgrading to DB version 1")
err := this.multiTx(
`CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY,
article INTEGER,
modified INTEGER,
body BLOB,
author TEXT
);`,
`CREATE TABLE IF NOT EXISTS titles (
id INTEGER PRIMARY KEY,
title TEXT
);`,
`CREATE INDEX IF NOT EXISTS articles_modified_index ON articles (modified)`,
`CREATE INDEX IF NOT EXISTS articles_title_index ON articles (article)`,
`INSERT INTO schema (id) VALUES (1);`,
)
if err != nil {
return err
}
currentSchema = 1
} }
_, err = this.db.Exec(` //
CREATE INDEX IF NOT EXISTS articles_title_index ON articles (article)
`) if currentSchema == 1 {
if err != nil { log.Println("Upgrading to DB version 2")
return err
err := this.multiTx(
`ALTER TABLE titles ADD COLUMN is_deleted INTEGER DEFAULT 0;`,
`INSERT INTO schema (id) VALUES (2);`,
)
if err != nil {
return err
}
currentSchema = 1
} }
//
return nil return nil
} }
type TitleInfo struct {
TitleID int64
Title string
IsDeleted bool
}
type Article struct { type Article struct {
ID int64 TitleInfo
TitleID int64 ArticleID int64
Modified int64 Modified int64
Body []byte Body []byte
Author string Author string
Title string
} }
func (this *WikiDB) GetArticleById(articleId int) (*Article, error) { func (this *WikiDB) GetArticleById(articleId int) (*Article, error) {
@@ -86,12 +137,12 @@ func (this *WikiDB) GetArticleById(articleId int) (*Article, error) {
} }
func (this *WikiDB) GetRevision(revId int) (*Article, error) { func (this *WikiDB) GetRevision(revId int) (*Article, error) {
row := this.db.QueryRow(`SELECT articles.*, titles.title FROM articles JOIN titles ON articles.article=titles.id WHERE articles.id = ?`, revId) row := this.db.QueryRow(`SELECT articles.*, titles.title, titles.is_deleted FROM articles JOIN titles ON articles.article=titles.id WHERE articles.id = ?`, revId)
return this.parseArticleWithTitle(row) return this.parseArticleWithTitle(row)
} }
func (this *WikiDB) GetLatestVersion(title string) (*Article, error) { 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) row := this.db.QueryRow(`SELECT articles.* FROM articles WHERE article = (SELECT id FROM titles WHERE title = ?) ORDER BY modified DESC LIMIT 1`, this.normaliseTitle(title))
return this.parseArticle(row) return this.parseArticle(row)
} }
@@ -103,6 +154,10 @@ 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) 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) normaliseTitle(title string) string {
return strings.ToLower(strings.Trim(title, " \r\n\t"))
}
func (this *WikiDB) SaveArticle(title, author, body string, expectBaseRev int64) error { func (this *WikiDB) SaveArticle(title, author, body string, expectBaseRev int64) error {
isNewArticle := false isNewArticle := false
a, err := this.GetLatestVersion(title) a, err := this.GetLatestVersion(title)
@@ -114,8 +169,8 @@ func (this *WikiDB) SaveArticle(title, author, body string, expectBaseRev int64)
} }
} }
if !isNewArticle && a.ID != expectBaseRev { if !isNewArticle && a.ArticleID != expectBaseRev {
return ArticleAlteredError{got: expectBaseRev, expected: a.ID} return ArticleAlteredError{got: expectBaseRev, expected: a.ArticleID}
} }
zBody, err := gzdeflate([]byte(body), this.compressionLevel) zBody, err := gzdeflate([]byte(body), this.compressionLevel)
@@ -125,7 +180,7 @@ func (this *WikiDB) SaveArticle(title, author, body string, expectBaseRev int64)
var titleId int64 var titleId int64
if isNewArticle { if isNewArticle {
titleInsert, err := this.db.Exec(`INSERT INTO titles (title) VALUES (?)`, title) titleInsert, err := this.db.Exec(`INSERT INTO titles (title) VALUES (?)`, this.normaliseTitle(title))
if err != nil { if err != nil {
return err return err
} }
@@ -143,11 +198,21 @@ func (this *WikiDB) SaveArticle(title, author, body string, expectBaseRev int64)
return err return err
} }
// Update is-deleted flag
isDeleted := 0
if body == bbcodeIsDeleted {
isDeleted = 1
}
_, err = this.db.Exec(`UPDATE titles SET is_deleted = ? WHERE id = ?`, isDeleted, titleId)
return nil 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`,
this.normaliseTitle(title),
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -156,7 +221,7 @@ func (this *WikiDB) GetRevisionHistory(title string) ([]Article, error) {
ret := make([]Article, 0) ret := make([]Article, 0)
for rows.Next() { for rows.Next() {
a := Article{} a := Article{}
err := rows.Scan(&a.ID, &a.Modified, &a.Author) err := rows.Scan(&a.ArticleID, &a.Modified, &a.Author)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -182,7 +247,7 @@ func (this *WikiDB) GetNextOldestRevision(revision int) (int, error) {
func (this *WikiDB) GetRecentChanges(offset int, limit int) ([]Article, error) { func (this *WikiDB) GetRecentChanges(offset int, limit int) ([]Article, error) {
rows, err := this.db.Query( rows, err := this.db.Query(
`SELECT articles.id, articles.modified, articles.author, titles.title FROM articles JOIN titles ON articles.article=titles.id ORDER BY modified DESC ` + `SELECT articles.id, articles.modified, articles.author, titles.title, titles.is_deleted FROM articles JOIN titles ON articles.article=titles.id ORDER BY modified DESC ` +
fmt.Sprintf(`LIMIT %d OFFSET %d`, limit, offset), fmt.Sprintf(`LIMIT %d OFFSET %d`, limit, offset),
) )
if err != nil { if err != nil {
@@ -193,7 +258,7 @@ func (this *WikiDB) GetRecentChanges(offset int, limit int) ([]Article, error) {
ret := make([]Article, 0, limit) ret := make([]Article, 0, limit)
for rows.Next() { for rows.Next() {
a := Article{} a := Article{}
err := rows.Scan(&a.ID, &a.Modified, &a.Author, &a.Title) err := rows.Scan(&a.ArticleID, &a.Modified, &a.Author, &a.Title, &a.IsDeleted)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -213,17 +278,23 @@ func (this *WikiDB) TotalRevisions() (int64, error) {
return ret, nil return ret, nil
} }
func (this *WikiDB) ListTitles() ([]string, error) { func (this *WikiDB) ListTitles(includeDeleted bool) ([]TitleInfo, error) {
rows, err := this.db.Query(`SELECT title FROM titles ORDER BY title ASC`) stmt := `SELECT id, title, is_deleted FROM titles`
if !includeDeleted {
stmt += ` WHERE is_deleted = 0`
}
stmt += ` ORDER BY title ASC`
rows, err := this.db.Query(stmt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
ret := make([]string, 0) ret := make([]TitleInfo, 0)
for rows.Next() { for rows.Next() {
var title string var title TitleInfo
err = rows.Scan(&title) err = rows.Scan(&title.TitleID, &title.Title, &title.IsDeleted)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -236,7 +307,7 @@ func (this *WikiDB) ListTitles() ([]string, error) {
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
err := row.Scan(&a.ID, &a.TitleID, &a.Modified, &gzBody, &a.Author) err := row.Scan(&a.ArticleID, &a.TitleID, &a.Modified, &gzBody, &a.Author)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -254,7 +325,7 @@ func (this *WikiDB) parseArticle(row *sql.Row) (*Article, error) {
func (this *WikiDB) parseArticleWithTitle(row *sql.Row) (*Article, error) { func (this *WikiDB) parseArticleWithTitle(row *sql.Row) (*Article, error) {
a := Article{} a := Article{}
var gzBody []byte var gzBody []byte
err := row.Scan(&a.ID, &a.TitleID, &a.Modified, &gzBody, &a.Author, &a.Title) err := row.Scan(&a.ArticleID, &a.TitleID, &a.Modified, &gzBody, &a.Author, &a.Title, &a.IsDeleted)
if err != nil { if err != nil {
return nil, err return nil, err
} }

15
Gopkg.lock generated Normal file
View File

@@ -0,0 +1,15 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/mattn/go-sqlite3"
packages = ["."]
revision = "6c771bb9887719704b210e87e934f08be014bdb1"
version = "v1.6.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "a1f2d643f8c1770c92ee1759184a0c7004af5672869db579328d05bb7cfd6bef"
solver-name = "gps-cdcl"
solver-version = 1

26
Gopkg.toml Normal file
View File

@@ -0,0 +1,26 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
[[constraint]]
name = "github.com/mattn/go-sqlite3"
version = "1.3.0"

View File

@@ -2,20 +2,24 @@
# Makefile for YATWiki3 # Makefile for YATWiki3
# #
VERSION:=3.0 VERSION:=3.3.0
SOURCES:=Makefile \ SOURCES:=Makefile \
static \ static \
cmd $(wildcard cmd/yatwiki-server/*.go) \ cmd $(wildcard cmd/yatwiki-server/*.go) \
Gopkg.lock Gopkg.toml \
$(wildcard *.go) $(wildcard *.go)
GOFLAGS := -ldflags='-s -w' -gcflags='-trimpath=$(GOPATH)' -asmflags='-trimpath=$(GOPATH)' GOFLAGS:=-a \
-ldflags "-s -w -X code.ivysaur.me/yatwiki.SERVER_HEADER=YATWiki/$(VERSION)" \
-gcflags '-trimpath=$(GOPATH)' \
-asmflags '-trimpath=$(GOPATH)'
# #
# Phony targets # Phony targets
# #
.PHONY: all dist clean .PHONY: all dist clean deps
all: build/linux64/yatwiki-server build/win32/yatwiki-server.exe all: build/linux64/yatwiki-server build/win32/yatwiki-server.exe
@@ -25,16 +29,21 @@ dist: \
_dist/yatwiki-$(VERSION)-src.zip _dist/yatwiki-$(VERSION)-src.zip
clean: clean:
if [ -f ./staticResources.go ] ; then rm ./staticResources.go ; fi rm -f ./staticResources.go
if [ -d ./build ] ; then rm -r ./build ; fi rm -fr ./build
if [ -f ./yatwiki ] ; then rm ./yatwiki ; fi rm -f ./yatwiki
deps:
go get -u github.com/jteeuwen/go-bindata/...
go get -u github.com/golang/dep/cmd/dep
dep ensure
# #
# Generated files # Generated files
# #
staticResources.go: static/ static/* staticResources.go: static/ static/*
go-bindata -o staticResources.go -prefix static -pkg yatwiki3 static go-bindata -o staticResources.go -prefix static -pkg yatwiki static
# #

View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"time" "time"
@@ -13,6 +13,7 @@ type ServerOptions struct {
DBFilePath string DBFilePath string
FaviconFilePath string FaviconFilePath string
AllowDBDownload bool AllowDBDownload bool
TrustXForwardedFor bool // Introduced in 3.0.1 - default false
RecentChanges int RecentChanges int
RecentChangesRSS int RecentChangesRSS int
GzipCompressionLevel int GzipCompressionLevel int
@@ -20,6 +21,8 @@ type ServerOptions struct {
ExternalBaseURL string ExternalBaseURL string
DeclareRSSLanguage string DeclareRSSLanguage string
DeclareRSSEmail string DeclareRSSEmail string
ContentedServer string
ContentedBBCodeTag string
} }
func DefaultOptions() *ServerOptions { func DefaultOptions() *ServerOptions {
@@ -32,6 +35,7 @@ func DefaultOptions() *ServerOptions {
DBFilePath: "wiki.db", DBFilePath: "wiki.db",
FaviconFilePath: "", // no favicon FaviconFilePath: "", // no favicon
AllowDBDownload: true, AllowDBDownload: true,
TrustXForwardedFor: false,
RecentChanges: 20, RecentChanges: 20,
RecentChangesRSS: 10, RecentChangesRSS: 10,
GzipCompressionLevel: 9, GzipCompressionLevel: 9,
@@ -39,5 +43,7 @@ func DefaultOptions() *ServerOptions {
ExternalBaseURL: "http://127.0.0.1/", ExternalBaseURL: "http://127.0.0.1/",
DeclareRSSLanguage: "en-GB", DeclareRSSLanguage: "en-GB",
DeclareRSSEmail: `nobody@example.com`, DeclareRSSEmail: `nobody@example.com`,
ContentedServer: "",
ContentedBBCodeTag: "",
} }
} }

View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"database/sql" "database/sql"
@@ -14,6 +14,8 @@ import (
"time" "time"
) )
var SERVER_HEADER string = "YATWiki/0.0.0-devel"
type WikiServer struct { type WikiServer struct {
db *WikiDB db *WikiDB
opts *ServerOptions opts *ServerOptions
@@ -37,7 +39,13 @@ func NewWikiServer(opts *ServerOptions) (*WikiServer, error) {
} }
} }
tmpl, err := template.New("yatwiki/page").Parse(pageTemplate) tmpl := template.New("yatwiki/page")
tmpl.Funcs(map[string]interface{}{
"pathcomponent": func(s string) string {
return url.PathEscape(s)
},
})
_, err = tmpl.Parse(pageTemplate)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -68,12 +76,16 @@ func NewWikiServer(opts *ServerOptions) (*WikiServer, error) {
return &ws, nil return &ws, nil
} }
func (this *WikiServer) GetBBCodeRenderer() *BBCodeRenderer {
return NewBBCodeRenderer(this.opts.ExpectBaseURL, this.opts.ContentedServer, this.opts.ContentedBBCodeTag)
}
func (this *WikiServer) Close() { func (this *WikiServer) Close() {
this.db.Close() this.db.Close()
} }
func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "YATWiki3") w.Header().Set("Server", SERVER_HEADER)
if len(this.bans) > 0 { if len(this.bans) > 0 {
remoteIP := RemoteAddrToIPAddress(r.RemoteAddr) remoteIP := RemoteAddrToIPAddress(r.RemoteAddr)
@@ -104,9 +116,13 @@ func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write(content) w.Write(content)
return return
} else if remainingPath == "favicon.ico" && len(this.opts.FaviconFilePath) > 0 { } else if remainingPath == "favicon.ico" {
w.Header().Set("Content-Type", "image/x-icon") if len(this.opts.FaviconFilePath) > 0 {
http.ServeFile(w, r, this.opts.FaviconFilePath) w.Header().Set("Content-Type", "image/x-icon")
http.ServeFile(w, r, this.opts.FaviconFilePath)
} else {
http.Error(w, "Not found", 404)
}
return return
} else if remainingPath == "download-database" { } else if remainingPath == "download-database" {
@@ -128,22 +144,22 @@ func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} else if remainingPath == "" { } else if remainingPath == "" {
this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.QueryEscape(this.opts.DefaultPage)) this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.PathEscape(this.opts.DefaultPage))
return return
} else if remainingPath == "random" { } else if remainingPath == "random" {
titles, err := this.db.ListTitles() titles, err := this.db.ListTitles(false) // "Random page" mode does not include deleted pages
if err != nil { if err != nil {
this.serveInternalError(w, r, err) this.serveInternalError(w, r, err)
return return
} }
chosenArticle := titles[rand.Intn(len(titles))] chosenArticle := titles[rand.Intn(len(titles))]
this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.QueryEscape(chosenArticle)) this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.PathEscape(chosenArticle.Title))
return return
} else if strings.HasPrefix(remainingPath, "view/") { } else if strings.HasPrefix(remainingPath, "view/") {
articleTitle, err := url.QueryUnescape(remainingPath[len("view/"):]) articleTitle, err := url.PathUnescape(remainingPath[len("view/"):])
if err != nil { if err != nil {
this.serveErrorMessage(w, err) this.serveErrorMessage(w, err)
return return
@@ -152,7 +168,7 @@ func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} else if strings.HasPrefix(remainingPath, "modify/") { } else if strings.HasPrefix(remainingPath, "modify/") {
articleTitle, err := url.QueryUnescape(remainingPath[len("modify/"):]) articleTitle, err := url.PathUnescape(remainingPath[len("modify/"):])
if err != nil { if err != nil {
this.serveErrorMessage(w, err) this.serveErrorMessage(w, err)
return return
@@ -161,7 +177,7 @@ func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} else if strings.HasPrefix(remainingPath, "history/") { } else if strings.HasPrefix(remainingPath, "history/") {
articleTitle, err := url.QueryUnescape(remainingPath[len("history/"):]) articleTitle, err := url.PathUnescape(remainingPath[len("history/"):])
if err != nil { if err != nil {
this.serveErrorMessage(w, err) this.serveErrorMessage(w, err)
return return
@@ -255,13 +271,13 @@ func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
err = this.db.SaveArticle(title, Author(r), body, int64(expectRev)) err = this.db.SaveArticle(title, Author(r, this.opts.TrustXForwardedFor), body, int64(expectRev))
if err != nil { if err != nil {
this.serveErrorMessage(w, err) this.serveErrorMessage(w, err)
return return
} }
this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.QueryEscape(title)) this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.PathEscape(title))
return return
} }

View File

@@ -1,6 +1,6 @@
A semi-anonymous wiki for use in trusted environments. A semi-anonymous wiki for use in trusted environments.
As of the 20150901 release, a desktop version is available for Windows (based on PHPDesktop). For the 20150901 release, a desktop version is available for Windows (based on PHPDesktop).
As of the 3.0 release, YATWiki is now a standalone server instead of a PHP script. As of the 3.0 release, YATWiki is now a standalone server instead of a PHP script.
@@ -14,6 +14,7 @@ As of the 3.0 release, YATWiki is now a standalone server instead of a PHP scrip
- IP-based ban system - IP-based ban system
- Article index, random article, download database backup - Article index, random article, download database backup
- Source code highlighting (thanks [url=https://github.com/isagalaev/highlight.js]highlight.js[/url]) - Source code highlighting (thanks [url=https://github.com/isagalaev/highlight.js]highlight.js[/url])
- Optional integration with `contented` for file/image uploads
Written in Golang, PHP Written in Golang, PHP
@@ -28,9 +29,54 @@ You can start YATWiki by running the binary. A default configuration file and da
Bind address (default "127.0.0.1:80") Bind address (default "127.0.0.1:80")
` `
=GO GET=
This package can be installed via go get: `go get code.ivysaur.me/yatwiki`
[go-get]code.ivysaur.me/yatwiki git https://git.ivysaur.me/code.ivysaur.me/yatwiki.git[/go-get]
=CHANGELOG= =CHANGELOG=
2017-07-11 v3.0 2018-04-02 3.3.0
- Feature: Allow deleting (and un-deleting) articles
- Feature: Support `[youtube]` tag for embedded Youtube videos
- Feature: Add link to view raw page source when viewing a specific page revision
- Feature: Display timestamps on all console log messages
- Enhancement: Upgrade bundled SQLite3 library
- Disable zooming out on mobile devices (but zooming in is still allowed)
- Fix an issue with linking to articles where the name contained an apostrophe
2017-11-18 3.2.0
- Feature: Add new ContentedBBCodeTag option to choose a BBCode tag for mini thumbnails (requires `contented` >= 1.2.0)
- Feature: Replace menu image with SVG, for high-DPI screens
- Feature: Hover over timestamps to display in more detail
- Feature: Link to diff pages directly from the Recent Changes page
- Fix some cosmetic issues with the Recent Changes page
2017-10-29 3.1.3
- Fix one more case of article title normalisation
2017-10-29 3.1.2
- Lock dependency versions
- Enhancement: Advertise build number in Server headers
- Fix a regression in 3.x series with not normalising article titles
- Fix server response if favicon is not configured
2017-10-15 3.1.1
- Update `contented` integration (requires `contented` >= 1.1.0)
2017-10-08 3.1.0
- Feature: Support content upload to a `contented` server
2017-08-11 3.0.2
- Fix an issue with XSS prevention for web browsers other than Chrome
2017-08-11 3.0.1
- Feature: New `TrustXForwardedFor` config option for usage behind reverse proxies
- Fix an issue with article titles containing `+`
- Fix an issue with `[html]` tags
- Fix an issue with viewing history for unknown articles
2017-07-11 3.0
- YATWiki was rewritten in Go. - YATWiki was rewritten in Go.
- Enhancement: Standalone binary server - Enhancement: Standalone binary server
- Enhancement: No longer requires cookies for error messages - Enhancement: No longer requires cookies for error messages
@@ -43,11 +89,13 @@ You can start YATWiki by running the binary. A default configuration file and da
- Fix a number of issues with handling of base URLs in links - Fix a number of issues with handling of base URLs in links
- Fix a cosmetic issue with file caching for CSS content - Fix a cosmetic issue with file caching for CSS content
2016-11-16 (no public release) 2016-11-16 20161116
- (no public release)
- Enhancement: Always open the formatting help in a new tab - Enhancement: Always open the formatting help in a new tab
- Fix a cosmetic issue with display of backslash characters caused by Meiryo font - Fix a cosmetic issue with display of backslash characters caused by Meiryo font
2016-08-24 (no public release) 2016-08-24 20160824
- (no public release)
- Feature: Add Compare button to both top and bottom of article revision list - Feature: Add Compare button to both top and bottom of article revision list
- Fix an issue with noncompliant HTML when comparing diffs - Fix an issue with noncompliant HTML when comparing diffs

100
bbcode.go
View File

@@ -1,75 +1,117 @@
package yatwiki3 package yatwiki
import ( import (
"encoding/json" "encoding/json"
"html"
"html/template" "html/template"
"net/url" "net/url"
"regexp" "regexp"
"strings" "strings"
) )
const (
bbcodeIsDeleted string = `[delete]`
)
// An embarassing cascade of half-working hacks follows. // An embarassing cascade of half-working hacks follows.
type BBCodeRenderer struct { type BBCodeRenderer struct {
baseUrl string baseUrl string
CodePresent bool CodePresent bool
DynamicContentWarning string DynamicContentWarning string
ContentedURL string
ContentedTag string
} }
func NewBBCodeRenderer(baseUrl string) *BBCodeRenderer { func NewBBCodeRenderer(baseUrl, ContentedURL, ContentedTag string) *BBCodeRenderer {
return &BBCodeRenderer{ return &BBCodeRenderer{
baseUrl: baseUrl, baseUrl: baseUrl,
CodePresent: false, CodePresent: false,
DynamicContentWarning: `⚠ run dynamic content`, DynamicContentWarning: `⚠ run dynamic content`,
ContentedURL: ContentedURL,
ContentedTag: ContentedTag,
} }
} }
func (this *BBCodeRenderer) Reset() {
this.CodePresent = false
}
//
type transformation interface {
MatchString(in string) bool
Apply(in string) string
}
type pregReplaceRule struct { type pregReplaceRule struct {
match *regexp.Regexp *regexp.Regexp
replace string replace string
}
func (this pregReplaceRule) Apply(in string) string {
return this.ReplaceAllString(in, this.replace)
}
type pregReplaceFunc struct {
*regexp.Regexp
replaceFunc func([]string) string replaceFunc func([]string) string
} }
func (this pregReplaceFunc) Apply(in string) string {
return PregReplaceCallback(this.Regexp, this.replaceFunc, in)
}
// Internal part of BBCode rendering. // Internal part of BBCode rendering.
// It handles most leaf-level tags. // It handles most leaf-level tags.
func (this *BBCodeRenderer) bbcode(data string) string { func (this *BBCodeRenderer) bbcode(data string) string {
s_to_r := []pregReplaceRule{ s_to_r := []transformation{
pregReplaceRule{regexp.MustCompile(`(?si)\[h\](.*?)\[/h\]`), `<h2>$1</h2>`, nil}, pregReplaceRule{regexp.MustCompile(`(?si)\[h\](.*?)\[/h\]`), `<h2>$1</h2>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[b\](.*?)\[/b\]`), `<b>$1</b>`, nil}, pregReplaceRule{regexp.MustCompile(`(?si)\[b\](.*?)\[/b\]`), `<b>$1</b>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[i\](.*?)\[/i\]`), `<i>$1</i>`, nil}, pregReplaceRule{regexp.MustCompile(`(?si)\[i\](.*?)\[/i\]`), `<i>$1</i>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[u\](.*?)\[/u\]`), `<u>$1</u>`, nil}, pregReplaceRule{regexp.MustCompile(`(?si)\[u\](.*?)\[/u\]`), `<u>$1</u>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[s\](.*?)\[/s\]`), `<span class="s">$1</span>`, nil}, pregReplaceRule{regexp.MustCompile(`(?si)\[s\](.*?)\[/s\]`), `<span class="s">$1</span>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[spoiler\](.*?)\[/spoiler\]`), `<span class="spoiler">$1</span>`, nil}, pregReplaceRule{regexp.MustCompile(`(?si)\[spoiler\](.*?)\[/spoiler\]`), `<span class="spoiler">$1</span>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[img\](.*?)\[/img\]`), `<img alt="" src="$1">`, nil}, pregReplaceRule{regexp.MustCompile(`(?si)\[img\](.*?)\[/img\]`), `<img alt="" src="$1">`},
pregReplaceRule{regexp.MustCompile(`(?si)\[list\](.*?)\[/list\]`), `<ul><li>$1</li></ul>`, nil}, pregReplaceRule{regexp.MustCompile(`(?si)\[list\](.*?)\[/list\]`), `<ul><li>$1</li></ul>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[\*\]`), `</li><li>`, nil}, pregReplaceRule{regexp.MustCompile(`(?si)\[\*\]`), `</li><li>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[url=(.*?)\](.*?)\[/url\]`), `<a rel="noreferrer" href="$1">$2</a>`, nil}, pregReplaceRule{regexp.MustCompile(`(?si)\[url=(.*?)\](.*?)\[/url\]`), `<a rel="noreferrer" href="$1">$2</a>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[article=(.*?)\](.*?)\[/article\]`), "", func(m []string) string { pregReplaceFunc{regexp.MustCompile(`(?si)\[article=(.*?)\](.*?)\[/article\]`), func(m []string) string {
return `<a href="` + template.HTMLEscapeString(this.baseUrl+`view/`+url.QueryEscape(m[1])) + `">` + m[2] + `</a>` // m[1] has already been hesc'd
// Need to unhesc, and then pathescape
targetArticle := html.UnescapeString(m[1])
return `<a href="` + template.HTMLEscapeString(this.baseUrl+`view/`+url.PathEscape(targetArticle)) + `">` + m[2] + `</a>`
}}, }},
pregReplaceRule{regexp.MustCompile(`(?si)\[rev=(.*?)\](.*?)\[/rev\]`), "", func(m []string) string { pregReplaceFunc{regexp.MustCompile(`(?si)\[rev=(.*?)\](.*?)\[/rev\]`), func(m []string) string {
return `<a href="` + template.HTMLEscapeString(this.baseUrl+`archive/`+url.QueryEscape(m[1])) + `">` + m[2] + `</a>` return `<a href="` + template.HTMLEscapeString(this.baseUrl+`archive/`+url.PathEscape(m[1])) + `">` + m[2] + `</a>`
}}, }},
pregReplaceRule{regexp.MustCompile(`(?si)\[imgur\](.*?)\.(...)\[/imgur\]`), pregReplaceRule{regexp.MustCompile(`(?si)\[imgur\](.*?)\.(...)\[/imgur\]`),
`<a href="https://i.imgur.com/${1}.${2}"><img class="imgur" alt="" src="https://i.imgur.com/${1}s.${2}" ></a>`, `<a href="https://i.imgur.com/${1}.${2}"><img class="imgur" alt="" src="https://i.imgur.com/${1}s.${2}" ></a>`,
nil,
}, },
pregReplaceRule{regexp.MustCompile(`(?si)\[section=(.*?)](.*?)\[/section\]`), "", func(m []string) string { pregReplaceRule{
regexp.MustCompile(`(?si)\[youtube](.*?)\[/youtube\]`),
`<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/${1}" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>`,
},
pregReplaceFunc{regexp.MustCompile(`(?si)\[section=(.*?)](.*?)\[/section\]`), func(m []string) string {
return `<div class="section"><a class="sectionheader" href="javascript:;" onclick="ts(this);">` + m[1] + `</a><span style="display:none;">` + strings.TrimSpace(m[2]) + `</span></div>` return `<div class="section"><a class="sectionheader" href="javascript:;" onclick="ts(this);">` + m[1] + `</a><span style="display:none;">` + strings.TrimSpace(m[2]) + `</span></div>`
}}, }},
} }
if len(this.ContentedTag) > 0 {
s_to_r = append(s_to_r,
pregReplaceRule{regexp.MustCompile(`(?si)\[` + regexp.QuoteMeta(this.ContentedTag) + `\](.*?)\[/` + regexp.QuoteMeta(this.ContentedTag) + `\]`),
`<a href="` + html.EscapeString(this.ContentedURL) + `p/${1}"><img class="imgur" alt="" src="` + html.EscapeString(this.ContentedURL) + `thumb/s/${1}" ></a>`,
},
)
}
for _, prr := range s_to_r { for _, prr := range s_to_r {
for prr.match.MatchString(data) { // repeat until all recursive replacements are consumed for prr.MatchString(data) { // repeat until all recursive replacements are consumed
if len(prr.replace) > 0 { data = prr.Apply(data)
data = prr.match.ReplaceAllString(data, prr.replace)
} else {
data = PregReplaceCallback(prr.match, prr.replaceFunc, data)
}
} }
} }
return data return data
@@ -177,7 +219,7 @@ func (this *BBCodeRenderer) displayfmt(s string) string {
} }
epos += spos epos += spos
jsonInnerContent, _ := json.Marshal(s[spos : epos-spos]) jsonInnerContent, _ := json.Marshal(s[spos:epos])
ret += `<div class="html"><a href="javascript:;" onclick="` + template.HTMLEscapeString(`els(this, `+string(jsonInnerContent)+`);`) + `">` + this.DynamicContentWarning + `</a></div>` ret += `<div class="html"><a href="javascript:;" onclick="` + template.HTMLEscapeString(`els(this, `+string(jsonInnerContent)+`);`) + `">` + this.DynamicContentWarning + `</a></div>`
hpos = epos + 7 hpos = epos + 7

41
bbcode_test.go Normal file
View File

@@ -0,0 +1,41 @@
package yatwiki
import (
"testing"
)
func TestBBCode(t *testing.T) {
bbr := NewBBCodeRenderer("http://BASE_URL/", "http://CONTENTED_URL/", "[CONTENTED]")
type testCase struct {
input, expected string
}
testCases := []testCase{
// Test basic functionality
testCase{
`identity`,
`identity`,
},
// Expected youtube format
testCase{
`[youtube]dQw4w9WgXcQ[/youtube]`,
`<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>`,
},
}
for _, tc := range testCases {
// BBCode renderers are stateful
bbr.Reset()
output := bbr.RenderHTML(tc.input)
if string(output) != tc.expected {
t.Fatalf("Test failed: %s\nResult:\n%s\n", tc.input, output)
}
}
}

View File

@@ -3,12 +3,12 @@ package main
import ( import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"os" "os"
"code.ivysaur.me/yatwiki3" "code.ivysaur.me/yatwiki"
) )
func main() { func main() {
@@ -17,45 +17,42 @@ func main() {
configPath := flag.String("config", "config.json", "Configuration file") configPath := flag.String("config", "config.json", "Configuration file")
flag.Parse() flag.Parse()
opts := yatwiki3.ServerOptions{} opts := yatwiki.ServerOptions{}
cfg, err := ioutil.ReadFile(*configPath) // Create default configuration file if necessary
if _, err := os.Stat(*configPath); os.IsNotExist(err) {
if err != nil { log.Printf("Creating default configuration file at '%s'...\n", *configPath)
if os.IsNotExist(err) { opts = *yatwiki.DefaultOptions()
opts = *yatwiki3.DefaultOptions() if cfg, err := json.MarshalIndent(opts, "", "\t"); err == nil {
if cfg, err := json.MarshalIndent(opts, "", "\t"); err == nil { err := ioutil.WriteFile(*configPath, cfg, 0644)
err := ioutil.WriteFile(*configPath, cfg, 0644) if err != nil {
if err != nil { log.Printf("Failed to save default configuration file: %s", err.Error())
fmt.Fprintf(os.Stderr, "Failed to save default configuration file: %s", err.Error())
}
} }
} else {
fmt.Fprintf(os.Stderr, "Failed to load configuration file '%s': %s\n", *configPath, err.Error())
os.Exit(1)
}
} else {
fmt.Printf("Loading configuration from '%s'...\n", *configPath)
err = json.Unmarshal(cfg, &opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse configuration file: %s\n", err.Error())
os.Exit(1)
} }
} }
ws, err := yatwiki3.NewWikiServer(&opts) // Load configuration
log.Printf("Loading configuration from '%s'...\n", *configPath)
cfg, err := ioutil.ReadFile(*configPath)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err.Error()) log.Fatalf("Failed to load configuration file '%s': %s\n", *configPath, err.Error())
os.Exit(1) }
err = json.Unmarshal(cfg, &opts)
if err != nil {
log.Fatalf("Failed to parse configuration file: %s\n", err.Error())
}
//
ws, err := yatwiki.NewWikiServer(&opts)
if err != nil {
log.Fatalln(err.Error())
} }
defer ws.Close() defer ws.Close()
fmt.Printf("YATWiki now listening on %s\n", *bindAddr) log.Printf("YATWiki now listening on %s\n", *bindAddr)
err = http.ListenAndServe(*bindAddr, ws) err = http.ListenAndServe(*bindAddr, ws)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err.Error()) log.Fatalln(err.Error())
os.Exit(1)
} }
os.Exit(0)
} }

View File

@@ -4,7 +4,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"code.ivysaur.me/yatwiki3/diff" "code.ivysaur.me/yatwiki/diff"
) )
func TestDiff(t *testing.T) { func TestDiff(t *testing.T) {

View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"bytes" "bytes"

View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"fmt" "fmt"
@@ -11,13 +11,17 @@ var subresourceNonce = time.Now().Unix()
type pageTemplateOptions struct { type pageTemplateOptions struct {
CurrentPageIsArticle bool CurrentPageIsArticle bool
CurrentPageName string CurrentPageName string
CurrentPageIsRev bool
CurrentPageRev int64
WikiTitle string WikiTitle string
Content template.HTML Content template.HTML
BaseURL string BaseURL string
LoadCodeResources bool LoadCodeResources bool
DefaultPage string DefaultPage string
AllowDownload bool AllowDownload bool
SessionMessage template.HTML SessionMessage string
PageNotExistsError bool
PageNotExistsTarget string
} }
func DefaultPageTemplateOptions(opts *ServerOptions) *pageTemplateOptions { func DefaultPageTemplateOptions(opts *ServerOptions) *pageTemplateOptions {
@@ -42,7 +46,7 @@ const pageTemplate string = `<!DOCTYPE html>
<head> <head>
<title>{{.CurrentPageName}}{{ if len .CurrentPageName }} - {{end}}{{.WikiTitle}}</title> <title>{{.CurrentPageName}}{{ if len .CurrentPageName }} - {{end}}{{.WikiTitle}}</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="referrer" content="no-referrer"> <meta name="referrer" content="no-referrer">
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <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="alternate" type="application/rss+xml" href="{{.BaseURL}}rss/changes" title="{{.WikiTitle}} - Recent Changes">
@@ -85,26 +89,63 @@ function els(e,s){ // no js exec in innerHTML
</head> </head>
<body> <body>
<div class="header"> <div class="header">
<a href="{{.BaseURL}}view/{{.DefaultPage | urlquery}}" title="Home"><div class="sprite hm"></div></a> <a href="{{.BaseURL}}view/{{.DefaultPage | pathcomponent}}" title="Home"><div class="sprite">
<a href="javascript:;" onclick="tid('spm');tid('tr1');tid('tr2');" title="Menu"><div class="sprite sp"></div></a> <svg viewBox="0 0 24 24">
<a href="{{.BaseURL}}modify/{{.NewArticleTitle | urlquery}}" title="New Page"><div class="sprite nw"></div></a> <path d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" />
</svg>
</div></a>
<a href="javascript:;" onclick="tid('spm');tid('tr1');tid('tr2');" title="Menu"><div class="sprite">
<svg viewBox="0 0 24 24">
<path d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z" />
</svg>
</div></a>
<a href="{{.BaseURL}}modify/{{.NewArticleTitle | pathcomponent}}" title="New Page"><div class="sprite">
<svg viewBox="0 0 24 24">
<path d="M13,9V3.5L18.5,9M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6Z" />
</svg>
</div></a>
{{if .CurrentPageIsArticle }} {{if .CurrentPageIsArticle }}
<div class="sep"></div> <div class="sep"></div>
<a href="{{.BaseURL}}history/{{.CurrentPageName | urlquery}}" title="Page History"><div class="sprite hs"></div></a> <a href="{{.BaseURL}}history/{{.CurrentPageName | pathcomponent}}" title="Page History"><div class="sprite">
<a href="{{.BaseURL}}modify/{{.CurrentPageName | urlquery}}" title="Modify Page"><div class="sprite ed"></div></a> <svg viewBox="0 0 24 24">
<path d="M15,13H16.5V15.82L18.94,17.23L18.19,18.53L15,16.69V13M19,8H5V19H9.67C9.24,18.09 9,17.07 9,16A7,7 0 0,1 16,9C17.07,9 18.09,9.24 19,9.67V8M5,21C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H6V1H8V3H16V1H18V3H19A2,2 0 0,1 21,5V11.1C22.24,12.36 23,14.09 23,16A7,7 0 0,1 16,23C14.09,23 12.36,22.24 11.1,21H5M16,11.15A4.85,4.85 0 0,0 11.15,16C11.15,18.68 13.32,20.85 16,20.85A4.85,4.85 0 0,0 20.85,16C20.85,13.32 18.68,11.15 16,11.15Z" />
</svg>
</div></a>
<a href="{{.BaseURL}}modify/{{.CurrentPageName | pathcomponent}}" title="Modify Page"><div class="sprite">
<svg viewBox="0 0 24 24">
<path d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z" />
</svg>
</div></a>
{{if .CurrentPageIsRev }}
<a href="{{.BaseURL}}raw/{{.CurrentPageRev}}" title="Page Source"><div class="sprite">
<svg viewBox="0 0 24 24">
<path d="M8,3A2,2 0 0,0 6,5V9A2,2 0 0,1 4,11H3V13H4A2,2 0 0,1 6,15V19A2,2 0 0,0 8,21H10V19H8V14A2,2 0 0,0 6,12A2,2 0 0,0 8,10V5H10V3M16,3A2,2 0 0,1 18,5V9A2,2 0 0,0 20,11H21V13H20A2,2 0 0,0 18,15V19A2,2 0 0,1 16,21H14V19H16V14A2,2 0 0,1 18,12A2,2 0 0,1 16,10V5H14V3H16Z" />
</svg>
</div></a>
{{end}}
{{end}} {{end}}
</div> </div>
<div id="tr1" style="display:none;"></div> <div id="tr1" style="display:none;"></div>
<div id="tr2" style="display:none;"></div> <div id="tr2" style="display:none;"></div>
<div class="ddmenu" id="spm" style="display:none;"> <div class="ddmenu" id="spm" style="display:none;">
<a href="{{.BaseURL}}recent/1"><div class="sprite no"></div> Recent Changes</a> <a href="{{.BaseURL}}recent/1"><div class="sprite"></div> Recent Changes</a>
<a href="{{.BaseURL}}random"><div class="sprite rn"></div> Random Page</a> <a href="{{.BaseURL}}random"><div class="sprite">
<a href="{{.BaseURL}}index"><div class="sprite no"></div> Article Index</a> <svg viewBox="0 0 24 24">
<path d="M12,18A6,6 0 0,1 6,12C6,11 6.25,10.03 6.7,9.2L5.24,7.74C4.46,8.97 4,10.43 4,12A8,8 0 0,0 12,20V23L16,19L12,15M12,4V1L8,5L12,9V6A6,6 0 0,1 18,12C18,13 17.75,13.97 17.3,14.8L18.76,16.26C19.54,15.03 20,13.57 20,12A8,8 0 0,0 12,4Z" />
</svg>
</div> Random Page</a>
<a href="{{.BaseURL}}index"><div class="sprite"></div> Article Index</a>
{{if .AllowDownload}} {{if .AllowDownload}}
<a href="{{.BaseURL}}download-database" download><div class="sprite no"></div> Download DB backup</a> <a href="{{.BaseURL}}download-database" download><div class="sprite"></div> Download DB backup</a>
{{end}} {{end}}
</div> </div>
<div class="content"> <div class="content">
{{if .PageNotExistsError}}
<div class="info">
No such article exists.
<a href="{{.BaseURL}}modify/{{.PageNotExistsTarget | pathcomponent}}">Click here</a> to create it.
</div>
{{end}}
{{if len .SessionMessage}} {{if len .SessionMessage}}
<div class="info">{{.SessionMessage}}</div> <div class="info">{{.SessionMessage}}</div>
{{end}} {{end}}

View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"database/sql" "database/sql"
@@ -25,13 +25,18 @@ func (this *WikiServer) routeArchive(w http.ResponseWriter, r *http.Request, rev
pto.CurrentPageName = a.Title pto.CurrentPageName = a.Title
pto.CurrentPageIsArticle = true pto.CurrentPageIsArticle = true
bcr := NewBBCodeRenderer(this.opts.ExpectBaseURL) pto.CurrentPageRev = int64(revId)
pto.CurrentPageIsRev = true
infoMessageHtml := `You are viewing a specific revision of this page, last modified ` +
time.Unix(a.Modified, 0).In(this.loc).Format(this.opts.DateFormat) + `. `
if !a.IsDeleted {
infoMessageHtml += `Click <a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(a.Title)) + `">here</a> to see the latest revision.`
}
bcr := this.GetBBCodeRenderer()
pto.Content = template.HTML( pto.Content = template.HTML(
`<div class="info">`+ `<div class="info">`+infoMessageHtml+`</div>`,
`You are viewing specific revision of this page, last modified `+
time.Unix(a.Modified, 0).In(this.loc).Format(this.opts.DateFormat)+`. `+
`Click <a href="`+template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.QueryEscape(a.Title))+`">here</a> to see the latest revision.`+
`</div>`,
) + bcr.RenderHTML(string(a.Body)) ) + bcr.RenderHTML(string(a.Body))
pto.LoadCodeResources = bcr.CodePresent pto.LoadCodeResources = bcr.CodePresent

View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"bytes" "bytes"
@@ -8,7 +8,7 @@ import (
"html/template" "html/template"
"net/http" "net/http"
"code.ivysaur.me/yatwiki3/diff" "code.ivysaur.me/yatwiki/diff"
) )
func (this *WikiServer) routeDiff(w http.ResponseWriter, r *http.Request, oldRev, newRev int) { func (this *WikiServer) routeDiff(w http.ResponseWriter, r *http.Request, oldRev, newRev int) {
@@ -53,9 +53,9 @@ func (this *WikiServer) routeDiff(w http.ResponseWriter, r *http.Request, oldRev
pto.Content = template.HTML( pto.Content = template.HTML(
`<h2>` + `<h2>` +
`Comparing ` + string(this.viewLink(oa.Title)) + ` versions ` + `Comparing ` + string(this.viewLink(oa.Title)) + ` versions ` +
`<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", oa.ID)) + `">r` + fmt.Sprintf("%d", oa.ID) + `</a>` + `<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", oa.ArticleID)) + `">r` + fmt.Sprintf("%d", oa.ArticleID) + `</a>` +
` - ` + ` - ` +
`<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", na.ID)) + `">r` + fmt.Sprintf("%d", na.ID) + `</a>` + `<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", na.ArticleID)) + `">r` + fmt.Sprintf("%d", na.ArticleID) + `</a>` +
`</h2>` + `</h2>` +
`<pre>` + string(b.Bytes()) + `</pre>`, `<pre>` + string(b.Bytes()) + `</pre>`,
) )

View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"html/template" "html/template"
@@ -8,12 +8,8 @@ import (
"time" "time"
) )
func (this *WikiServer) noSuchArticleError(title string) template.HTML { func (this *WikiServer) serveErrorMessage(w http.ResponseWriter, err error) {
return template.HTML(`No such article exists. <a href="` + this.opts.ExpectBaseURL + `modify/` + template.HTMLEscapeString(url.QueryEscape(title)) + `">Click here</a> to create it.`) this.serveErrorText(w, err.Error())
}
func (this *WikiServer) serveErrorMessage(w http.ResponseWriter, message error) {
this.serveErrorHTMLMessage(w, template.HTML(template.HTMLEscapeString(message.Error())))
} }
func (this *WikiServer) serveInternalError(w http.ResponseWriter, r *http.Request, e error) { func (this *WikiServer) serveInternalError(w http.ResponseWriter, r *http.Request, e error) {
@@ -21,8 +17,16 @@ func (this *WikiServer) serveInternalError(w http.ResponseWriter, r *http.Reques
http.Error(w, "An internal error occurred. Please ask an administrator to check the log file.", 500) http.Error(w, "An internal error occurred. Please ask an administrator to check the log file.", 500)
} }
func (this *WikiServer) serveErrorHTMLMessage(w http.ResponseWriter, msg template.HTML) { func (this *WikiServer) serveDeleted(w http.ResponseWriter, lookingFor string) {
this.serveRedirect(w, this.opts.ExpectBaseURL+"view/"+url.QueryEscape(this.opts.DefaultPage)+"?error="+url.QueryEscape(string(msg))) this.serveRedirect(w, this.opts.ExternalBaseURL+"history/"+url.PathEscape(lookingFor)+"?error="+url.QueryEscape(`The page you are looking for has been deleted. However, the history is still available.`))
}
func (this *WikiServer) serveErrorText(w http.ResponseWriter, msg string) {
this.serveRedirect(w, this.opts.ExpectBaseURL+"view/"+url.PathEscape(this.opts.DefaultPage)+"?error="+url.QueryEscape(msg))
}
func (this *WikiServer) serveNoSuchArticle(w http.ResponseWriter, lookingFor string) {
this.serveRedirect(w, this.opts.ExpectBaseURL+"view/"+url.PathEscape(this.opts.DefaultPage)+"?notfound="+url.QueryEscape(lookingFor))
} }
func (this *WikiServer) serveRedirect(w http.ResponseWriter, location string) { func (this *WikiServer) serveRedirect(w http.ResponseWriter, location string) {
@@ -32,7 +36,14 @@ func (this *WikiServer) serveRedirect(w http.ResponseWriter, location string) {
func (this *WikiServer) servePageResponse(w http.ResponseWriter, r *http.Request, pto *pageTemplateOptions) { func (this *WikiServer) servePageResponse(w http.ResponseWriter, r *http.Request, pto *pageTemplateOptions) {
w.WriteHeader(200) w.WriteHeader(200)
pto.SessionMessage = template.HTML(r.URL.Query().Get("error")) // FIXME reflected XSS (although Chrome automatically blocks it..)
if noSuchArticleTarget, ok := r.URL.Query()["notfound"]; ok {
pto.PageNotExistsError = true
pto.PageNotExistsTarget = noSuchArticleTarget[0]
} else {
pto.SessionMessage = r.URL.Query().Get("error")
}
err := this.pageTmp.Execute(w, pto) err := this.pageTmp.Execute(w, pto)
if err != nil { if err != nil {
@@ -40,11 +51,13 @@ func (this *WikiServer) servePageResponse(w http.ResponseWriter, r *http.Request
} }
} }
func (this *WikiServer) formatTimestamp(m int64) string { func (this *WikiServer) formatTimestamp(m int64) template.HTML {
// TODO add a more detailed timestamp on hover // TODO add a more detailed timestamp on hover
return template.HTMLEscapeString(time.Unix(m, 0).In(this.loc).Format(this.opts.DateFormat)) dt := time.Unix(m, 0).In(this.loc)
return template.HTML(`<span title="` + template.HTMLEscapeString(dt.Format(time.RFC3339)) + `">` + template.HTMLEscapeString(dt.Format(this.opts.DateFormat)) + `</span>`)
} }
func (this *WikiServer) viewLink(articleTitle string) template.HTML { func (this *WikiServer) viewLink(articleTitle string) template.HTML {
return template.HTML(`&quot;<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.QueryEscape(articleTitle)) + `">` + template.HTMLEscapeString(articleTitle) + `</a>&quot;`) return template.HTML(`&quot;<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(articleTitle)) + `">` + template.HTMLEscapeString(articleTitle) + `</a>&quot;`)
} }

View File

@@ -1,13 +1,15 @@
package yatwiki3 package yatwiki
import ( import (
"html/template"
"net/http" "net/http"
) )
func (this *WikiServer) routeFormatting(w http.ResponseWriter, r *http.Request) { func (this *WikiServer) routeFormatting(w http.ResponseWriter, r *http.Request) {
pto := DefaultPageTemplateOptions(this.opts) pto := DefaultPageTemplateOptions(this.opts)
pto.CurrentPageName = "Formatting help" pto.CurrentPageName = "Formatting help"
pto.Content = `
content := `
<h2>Formatting help</h2><br><br> <h2>Formatting help</h2><br><br>
<ul> <ul>
<li>[h]header[/h]</li> <li>[h]header[/h]</li>
@@ -21,9 +23,22 @@ func (this *WikiServer) routeFormatting(w http.ResponseWriter, r *http.Request)
<li>[article=page name]title[/article] or [rev=id]title[/rev]</li> <li>[article=page name]title[/article] or [rev=id]title[/rev]</li>
<li>[img]image-url[/img]</li> <li>[img]image-url[/img]</li>
<li>[imgur]asdf.jpg[/imgur]</li> <li>[imgur]asdf.jpg[/imgur]</li>
`
if len(this.opts.ContentedBBCodeTag) > 0 {
content += `
<li>[` + this.opts.ContentedBBCodeTag + `]abc[/` + this.opts.ContentedBBCodeTag + `]</li>
`
}
content += `
<li>[code]fixed width[/code]</li> <li>[code]fixed width[/code]</li>
<li>[section=header]content[/section]</li> <li>[section=header]content[/section]</li>
<li>[youtube]id[/youtube]</li>
<li>[html]raw html[/html]</li> <li>[html]raw html[/html]</li>
<li>` + bbcodeIsDeleted + `</li>
</ul>` </ul>`
pto.Content = template.HTML(content)
this.servePageResponse(w, r, pto) this.servePageResponse(w, r, pto)
} }

View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"database/sql" "database/sql"
@@ -12,7 +12,7 @@ func (this *WikiServer) routeHistory(w http.ResponseWriter, r *http.Request, art
revs, err := this.db.GetRevisionHistory(articleTitle) revs, err := this.db.GetRevisionHistory(articleTitle)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
this.serveErrorHTMLMessage(w, this.noSuchArticleError(articleTitle)) this.serveNoSuchArticle(w, articleTitle)
return return
} }
@@ -20,21 +20,26 @@ func (this *WikiServer) routeHistory(w http.ResponseWriter, r *http.Request, art
return return
} }
if len(revs) == 0 {
this.serveNoSuchArticle(w, articleTitle)
return
}
pto := DefaultPageTemplateOptions(this.opts) pto := DefaultPageTemplateOptions(this.opts)
pto.CurrentPageName = articleTitle pto.CurrentPageName = articleTitle
pto.CurrentPageIsArticle = true pto.CurrentPageIsArticle = true
content := `<h2>Page History</h2><br>` + content := `<h2>Page History</h2><br>` +
`<em>There have been ` + fmt.Sprintf("%d", len(revs)) + ` edits to the page &quot;<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.QueryEscape(articleTitle)) + `">` + template.HTMLEscapeString(articleTitle) + `</a>&quot;.</em>` + `<em>There have been ` + fmt.Sprintf("%d", len(revs)) + ` edits to the page &quot;<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(articleTitle)) + `">` + template.HTMLEscapeString(articleTitle) + `</a>&quot;.</em>` +
`<br><br>` + `<br><br>` +
`<form method="GET" action="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`diff`) + `">` + `<form method="GET" action="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`diff`) + `">` +
`<table>` `<table>`
compareRow := `<tr><td colspan="2"></td><td><input type="submit" value="Compare Selected &raquo;"></td></tr>` compareRow := `<tr><td colspan="2"></td><td><input type="submit" value="Compare Selected &raquo;"></td></tr>`
content += compareRow content += compareRow
for _, rev := range revs { for _, rev := range revs {
revIdStr := fmt.Sprintf("%d", rev.ID) revIdStr := fmt.Sprintf("%d", rev.ArticleID)
content += `<tr>` + content += `<tr>` +
`<td><a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+revIdStr) + `">` + this.formatTimestamp(rev.Modified) + `</a></td>` + `<td><a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+revIdStr) + `">` + string(this.formatTimestamp(rev.Modified)) + `</a></td>` +
`<td>` + template.HTMLEscapeString(rev.Author) + `</td>` + `<td>` + template.HTMLEscapeString(rev.Author) + `</td>` +
`<td><input type="radio" name="t" value="` + revIdStr + `">&nbsp;<input type="radio" name="f" value="` + revIdStr + `"></td>` + `<td><input type="radio" name="t" value="` + revIdStr + `">&nbsp;<input type="radio" name="f" value="` + revIdStr + `"></td>` +
`</tr>` `</tr>`

View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"fmt" "fmt"
@@ -8,12 +8,15 @@ import (
) )
func (this *WikiServer) routeIndex(w http.ResponseWriter, r *http.Request) { func (this *WikiServer) routeIndex(w http.ResponseWriter, r *http.Request) {
titles, err := this.db.ListTitles() titles, err := this.db.ListTitles(true) // Always load deleted pages, even if we don't display them in the list
if err != nil { if err != nil {
this.serveInternalError(w, r, err) this.serveInternalError(w, r, err)
return return
} }
showDeleted := (r.FormValue("deleted") == "1")
anyDeleted := false
totalRevs, err := this.db.TotalRevisions() totalRevs, err := this.db.TotalRevisions()
if err != nil { if err != nil {
this.serveInternalError(w, r, err) this.serveInternalError(w, r, err)
@@ -22,10 +25,27 @@ func (this *WikiServer) routeIndex(w http.ResponseWriter, r *http.Request) {
content := fmt.Sprintf(`<h2>Article Index</h2><br><em>There are %d edits to %d pages.</em><br><br><ul>`, totalRevs, len(titles)) content := fmt.Sprintf(`<h2>Article Index</h2><br><em>There are %d edits to %d pages.</em><br><br><ul>`, totalRevs, len(titles))
for _, title := range titles { for _, title := range titles {
content += `<li><a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.QueryEscape(title)) + `">` + template.HTMLEscapeString(title) + `</a></li>` classAttr := ""
if title.IsDeleted {
anyDeleted = true
if !showDeleted {
continue
}
classAttr = `class="deleted"`
}
content += `<li><a ` + classAttr + ` href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(title.Title)) + `">` + template.HTMLEscapeString(title.Title) + `</a></li>`
} }
content += `</ul>` content += `</ul>`
if anyDeleted {
content += `<br>`
if !showDeleted {
content += `<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`index?deleted=1`) + `">Show deleted pages</a>`
} else {
content += `<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`index`) + `">Hide deleted pages</a>`
}
}
pto := DefaultPageTemplateOptions(this.opts) pto := DefaultPageTemplateOptions(this.opts)
pto.CurrentPageName = "Index" pto.CurrentPageName = "Index"
pto.Content = template.HTML(content) pto.Content = template.HTML(content)

View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"database/sql" "database/sql"
@@ -32,8 +32,8 @@ func (this *WikiServer) routeModify(w http.ResponseWriter, r *http.Request, arti
pageTitleHTML = `Creating new article` pageTitleHTML = `Creating new article`
baseRev = 0 baseRev = 0
} else { } else {
pageTitleHTML = `Editing article &quot;<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.QueryEscape(articleTitle)) + `">` + template.HTMLEscapeString(articleTitle) + `</a>&quot;` pageTitleHTML = `Editing article &quot;<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(articleTitle)) + `">` + template.HTMLEscapeString(articleTitle) + `</a>&quot;`
baseRev = a.ID baseRev = a.ArticleID
existingBody = string(a.Body) existingBody = string(a.Body)
} }
@@ -49,6 +49,34 @@ func (this *WikiServer) routeModify(w http.ResponseWriter, r *http.Request, arti
</label> </label>
<input type="submit" value="Save &raquo;"> <input type="submit" value="Save &raquo;">
| <a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`formatting`) + `" target="_blank">formatting&nbsp;help</a> | <a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`formatting`) + `" target="_blank">formatting&nbsp;help</a>
`
if len(this.opts.ContentedServer) > 0 {
content += `
<script type="text/javascript" src="` + this.opts.ContentedServer + `sdk.js"></script>
| <a href="javascript:;" id="open-contented-uploader">upload...</a>
<script type="text/javascript">
document.getElementById("open-contented-uploader").addEventListener("click", function() {
contented.init("#contentctr", function(items) {
for (var i = 0; i < items.length; ++i) {
$("#contentctr textarea").append(" " + contented.getPreviewURL(items[i]) + " ");
}
});
});
</script>
`
}
content += `
|
<a href="javascript:;" id="delete-page">delete</a>
<script type="text/javascript">
document.getElementById("delete-page").addEventListener("click", function() {
if (confirm('Are you sure you want to delete this page?\nThe history will be preserved.')) {
document.getElementsByName("content")[0].value = '[delete]';
document.getElementById("form-edit-page").submit();
}
});
</script>
</div> </div>
<div id="contentctr"><textarea name="content">` + template.HTMLEscapeString(existingBody) + `</textarea></div> <div id="contentctr"><textarea name="content">` + template.HTMLEscapeString(existingBody) + `</textarea></div>
</form> </form>

14
rRSS.go
View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"fmt" "fmt"
@@ -27,17 +27,17 @@ func (this *WikiServer) routeRecentChangesRSS(w http.ResponseWriter, r *http.Req
for _, a := range recents { for _, a := range recents {
content += ` content += `
<item> <item>
<title>` + template.HTMLEscapeString(a.Title+` (r`+fmt.Sprintf("%d", a.ID)+`)`) + `</title> <title>` + template.HTMLEscapeString(a.Title+` (r`+fmt.Sprintf("%d", a.ArticleID)+`)`) + `</title>
<link>` + template.HTMLEscapeString(this.opts.ExternalBaseURL+`archive/`+fmt.Sprintf("%d", a.ID)) + `</link> <link>` + template.HTMLEscapeString(this.opts.ExternalBaseURL+`archive/`+fmt.Sprintf("%d", a.ArticleID)) + `</link>
<guid>` + template.HTMLEscapeString(this.opts.ExternalBaseURL+`archive/`+fmt.Sprintf("%d", a.ID)) + `</guid> <guid>` + template.HTMLEscapeString(this.opts.ExternalBaseURL+`archive/`+fmt.Sprintf("%d", a.ArticleID)) + `</guid>
<author>` + template.HTMLEscapeString(this.opts.DeclareRSSEmail+` (`+this.opts.PageTitle+` `+a.Author+`)`) + `</author> <author>` + template.HTMLEscapeString(this.opts.DeclareRSSEmail+` (`+this.opts.PageTitle+` `+a.Author+`)`) + `</author>
<pubDate>` + template.HTMLEscapeString(time.Unix(a.Modified, 0).In(this.loc).Format(time.RFC1123Z)) + `</pubDate> <pubDate>` + template.HTMLEscapeString(time.Unix(a.Modified, 0).In(this.loc).Format(time.RFC1123Z)) + `</pubDate>
<description>` + template.HTMLEscapeString(` <description>` + template.HTMLEscapeString(`
<a href="`+template.HTMLEscapeString(this.opts.ExternalBaseURL+`view/`+url.QueryEscape(a.Title))+`">latest version</a> <a href="`+template.HTMLEscapeString(this.opts.ExternalBaseURL+`view/`+url.PathEscape(a.Title))+`">latest version</a>
| |
<a href="`+template.HTMLEscapeString(this.opts.ExternalBaseURL+`archive/`+fmt.Sprintf("%d", a.ID))+`">revision `+fmt.Sprintf("%d", a.ID)+`</a> <a href="`+template.HTMLEscapeString(this.opts.ExternalBaseURL+`archive/`+fmt.Sprintf("%d", a.ArticleID))+`">revision `+fmt.Sprintf("%d", a.ArticleID)+`</a>
| |
<a href="`+template.HTMLEscapeString(this.opts.ExternalBaseURL+`diff/parent/`+fmt.Sprintf("%d", a.ID))+`">diff to previous</a> <a href="`+template.HTMLEscapeString(this.opts.ExternalBaseURL+`diff/parent/`+fmt.Sprintf("%d", a.ArticleID))+`">diff to previous</a>
`) + `</description> `) + `</description>
</item> </item>
` `

View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"database/sql" "database/sql"

View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"errors" "errors"
@@ -37,25 +37,46 @@ func (this *WikiServer) routeRecentChanges(w http.ResponseWriter, r *http.Reques
pto.CurrentPageName = "Recent Changes" pto.CurrentPageName = "Recent Changes"
content := `<h2>Recent Changes</h2><br>` + content := `<h2>Recent Changes</h2><br>` +
`<em>Showing up to ` + fmt.Sprintf("%d", this.opts.RecentChanges) + ` changes.</em><br>` + `<em>Showing up to ` + fmt.Sprintf("%d", this.opts.RecentChanges) + ` changes.</em><br><br>` +
`<table>` `<div style="display:inline-block;">` +
`<table class="ti">` +
`<tr><td>Page</td><td>Actions</td><td>Time</td><td>Author</td></tr>`
for _, rev := range recents { for _, rev := range recents {
diffHtml := ""
diffRev, err := this.db.GetNextOldestRevision(int(rev.ArticleID))
if err != nil {
diffHtml = `[new]`
} else {
diffHtml = `<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`diff/`+fmt.Sprintf("%d/%d", diffRev, rev.ArticleID)) + `">diff</a>`
}
classAttr := ""
if rev.IsDeleted {
classAttr = `class="deleted"`
}
content += `<tr>` + content += `<tr>` +
`<td><a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.QueryEscape(rev.Title)) + `">` + template.HTMLEscapeString(rev.Title) + `</a>` + `<td><a ` + classAttr + ` href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(rev.Title)) + `">` + template.HTMLEscapeString(rev.Title) + `</a></td>` +
` [<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", rev.ID)) + `">a</a>]` + `<td>` +
`<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", rev.ArticleID)) + `">rev</a> &nbsp; ` +
diffHtml +
`</td>` + `</td>` +
`<td>` + this.formatTimestamp(rev.Modified) + ` by ` + template.HTMLEscapeString(rev.Author) + `</td>` + `</td>` +
`<td>` + string(this.formatTimestamp(rev.Modified)) + `</td>` +
`<td>` + template.HTMLEscapeString(rev.Author) + `</td>` +
`</tr>` `</tr>`
} }
content += `<tr><td>` content += `</table>`
if pageNum > 1 { if pageNum > 1 {
content += `<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`recent/`+fmt.Sprintf("%d", pageNum-1)) + `">&laquo; Newer</a>` content += `<span style="float:left;"><a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`recent/`+fmt.Sprintf("%d", pageNum-1)) + `">&laquo; Newer</a></span>`
} }
content += `</td><td></td><td style="text-align:right;">`
if pageNum < maxPage { if pageNum < maxPage {
content += `<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`recent/`+fmt.Sprintf("%d", pageNum+1)) + `">Older &raquo;</a>` content += `<span style="float:right;"><a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`recent/`+fmt.Sprintf("%d", pageNum+1)) + `">Older &raquo;</a></span>`
} }
content += `</td></tr></table>` content += `</div>`
pto.Content = template.HTML(content) pto.Content = template.HTML(content)
this.servePageResponse(w, r, pto) this.servePageResponse(w, r, pto)

View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"database/sql" "database/sql"
@@ -15,22 +15,27 @@ func (this *WikiServer) routeView(w http.ResponseWriter, r *http.Request, articl
// If this was an old link, it might not be present. // If this was an old link, it might not be present.
// Redirect if possible // Redirect if possible
if len(articleTitle) > 0 && articleTitle[len(articleTitle)-1] == '/' { if len(articleTitle) > 0 && articleTitle[len(articleTitle)-1] == '/' {
this.serveRedirect(w, this.opts.ExpectBaseURL+"view/"+url.QueryEscape(articleTitle[0:len(articleTitle)-1])) this.serveRedirect(w, this.opts.ExpectBaseURL+"view/"+url.PathEscape(articleTitle[0:len(articleTitle)-1]))
return return
} }
this.serveErrorHTMLMessage(w, this.noSuchArticleError(articleTitle)) this.serveNoSuchArticle(w, articleTitle)
return return
} }
this.serveErrorMessage(w, err) this.serveErrorMessage(w, err)
return return
} }
if string(a.Body) == bbcodeIsDeleted {
this.serveDeleted(w, articleTitle)
return
}
pto := DefaultPageTemplateOptions(this.opts) pto := DefaultPageTemplateOptions(this.opts)
pto.CurrentPageName = articleTitle pto.CurrentPageName = articleTitle
pto.CurrentPageIsArticle = true pto.CurrentPageIsArticle = true
bcr := NewBBCodeRenderer(this.opts.ExpectBaseURL) bcr := this.GetBBCodeRenderer()
pto.Content = bcr.RenderHTML(string(a.Body)) pto.Content = bcr.RenderHTML(string(a.Body))
pto.LoadCodeResources = bcr.CodePresent pto.LoadCodeResources = bcr.CodePresent

View File

@@ -1,4 +1,4 @@
package yatwiki3 package yatwiki
import ( import (
"regexp" "regexp"

View File

@@ -18,6 +18,11 @@ td {padding:0px 10px;}
.spoiler{color:black;background-color:black;} .spoiler{color:black;background-color:black;}
.spoiler:hover{color:white;} .spoiler:hover{color:white;}
a.deleted {
color:red;
text-decoration:line-through;
}
.imgur { .imgur {
border:1px solid white; border:1px solid white;
width:90px; width:90px;
@@ -112,16 +117,6 @@ fieldset legend {
display:inline-block; display:inline-block;
width:16px;height:16px; width:16px;height:16px;
vertical-align:text-bottom; vertical-align:text-bottom;
background-repeat:no-repeat;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAwCAYAAABwrHhvAAAGS0lEQVRYw52Y32sVVxDHl4JI+2ApwUj7YCmC0ioJIVQI0YpSUwlRoxQlInmQoBEjBpsI/gR/Iv5Co9GYqKiXoCCiL0pBRUViNBA3EBAlBIwvvvsPnJ7P4cwyd3fv7k0XvnfnnvnO7OyPc2bOBMaY4O7duwncunUrvH37tgHIekz+YyvQ/DSg13yB+ykUCkXo7+8PBwYGzOTkpAMyY1euXHH/OcPTjm7cuBHxNS5evBidL1++HKYGcO3atQgXLlwIz507Z8bHx83nz58dkBk7deqUeffunTl27JiBqx1dvXrVfPz4MYF9+/aZ169fOyAfOXIkTARw6dIlh+PHj4eHDh1yZKJGBsiMIXOhnTt3GvjaUU9Pjws0jtbWVhe0gP+JAE6fPh3s2bMnxPGjR4/M+/fv3UVu3rzpgMwYOuQtW7YYbLQjeTpxnDx50ixZsiQCtokAtm7dGra0tLiLYYTMOxsdHXVAZgwdHGRstKPDhw9HjzoL8BIBrFy5MiIg846HhoaKwFicpx3ZJ2iePXuWC3iJANasWRPW1tYasHfvXvPkyZNUoBMeNtqRvL48wEsEsGvXrmD79u3m4cOHEe7cuRPNYWStg4uNdsS7vXfvXi5SvwGcbd682QwODkaQOS/zXuvgxgPYuHFj9NFmAV4iADu9grVr17opJuCdy5eMrHVwsdGOGhsbXaB5gJcIwL7bYPny5W6xEeh3iqx1cLHRjpYtW2bOnDmTC3iJALq7u4PFixcXffXr1q0zIyMjDshaBxcb7YgP89WrV7mAlwhg9+7dwerVq8OqqiojgFhXV+eArHVwsdGOmpqawoULF5o8wEsEYI/5FrUWSy3qLBosGmP40+J3j0XYFDkKgl+9rt6iOQaxrbFYkDoLgD2+taj0mAvZ42c/9gMc4ccC4JhhUWHxo7cj0F+87fden8yGPp9/zcrnsdz+NS23/x9EguTtcgB3OheJFzepAei8nQe407k4AX/58iW1MImI8bydhXhOL4Xr16+HVEr37993QUtdoW+gaC3XeTsL8fU8DX19fWFvb6/58OGDgw0mKk70DUw7n6fl9DjOnz8fsuqNjY0VgYC4gebm5uQriOfz+JGV0zVOnDgRpt0Mj59x9KkfYTyfU2IJOLJyuuDAgQNhV1eXefr0aRF4BYyjLzkNS+VzDorRrJwOOjs7w23btpkHDx4UgcKUcfSZ60BaPuewa35uTpd5/vjx4yKe1Jfx+jE1gLR8ziEyX7EsJkePHi0KQG8+mHKc37x543L/pk2bwrJWwrR8znQRmTqAi7CgIGsn8c0Hd08W1V97bgB5+Vz2Bpx5rNpJ2uaDlD2tXJCXz7kjSnHO8ZzOneqFqpw7TwSg8rnUA20W/1jssPjb1wroq1NqgQVet9RD1xFNFn95v3CqtH3kxNcD3/icP8fn8UU+sJ98np8Fp0QtEHh9pbef523n+foA+++EGNlOTU2VhC3BW0EWJ69HUKovkBuANW6VxQS5FE96CfGeArMFMJYZwKdPn1JhHY5LFYxciie9BDlLX0FKePoNmQHYaBOwi858NiDiEJmxNK70E3TSYYxeQ1mzYGJiIrArXb2tUlpttH0WBYt/efQyr5EZ87o+uNhgG09i/KfXUPY0tA7rbf52yyfVCx8O+78XL15Ed4XMGDo4cLHBVvoKGnnrf1EAZ8+erWZtZ+0eHh7O3d/DgYsNtrpnoKH7B6U6aIwHtlIJDh48OGDTpesB8NGV2tujgwMXG2x1b0FD9w+YCVKUajAejI6OOuzfv7+7o6Njkkddam+PDg5csSvnMUvnTTpuAsaCt2/fRrCO6ykeSu3t0cHRNuV+bLoDJ+B/YIUI7e3tvZRO5H+mHgUGQGYMHRxtU87FdQdOunAyYwKbaiOsX78+pA9UKBTcB0eLDSAzhg6Otsm6sHTf9EyRKknOwcuXLyPYVPpVphvVjL3YEECW6QlH22QFkDZDGNuwYUMo5+D58+cONsPNqqmpcQXnqlWrJtra2jpFh8wYOjhwRZcVQNoMiXfXnBOfMqsrKipWNDQ09Fp5RUp/YIUNomf27NlLfU6f523n+31/vc/5f6TYNvqaoN7XBPQifnPXlb2+rwdkf1/pawDpD8xV/YGZMRs5Znp9paondH9hjqopZojRf/M9B2Tz737/AAAAAElFTkSuQmCC);
}
.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; background:none;
} }
.sep { .sep {

File diff suppressed because one or more lines are too long