Compare commits

...

48 Commits

Author SHA1 Message Date
206ea45115 doc: top-level README.md 2018-12-31 18:56:19 +13:00
f2a69d1ed7 convert to Go Modules 2018-12-31 18:52:59 +13:00
7a6456aecc deletion: add missing form id for auto deletion 2018-04-02 18:38:56 +12:00
e44607b429 makefile: set version to 3.3.1 2018-04-02 18:16:20 +12:00
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
25 changed files with 500 additions and 187 deletions

3
.gitignore vendored
View File

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

161
DB.go
View File

@ -3,6 +3,8 @@ package yatwiki
import (
"database/sql"
"fmt"
"log"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
@ -32,52 +34,101 @@ func NewWikiDB(dbFilePath string, compressionLevel int) (*WikiDB, error) {
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 {
_, err := this.db.Exec(`
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY,
article INTEGER,
modified INTEGER,
body BLOB,
author TEXT
CREATE TABLE IF NOT EXISTS schema (
id INTEGER PRIMARY KEY
);`)
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
// Look up current value from schema table
schemaLookup := this.db.QueryRow(`SELECT MAX(id) mid FROM schema`)
currentSchema := int64(0)
if err = schemaLookup.Scan(&currentSchema); err == nil {
// That's fine
}
_, err = this.db.Exec(`
CREATE INDEX IF NOT EXISTS articles_modified_index ON articles (modified)
`)
if err != nil {
return err
log.Printf("Found DB version %d\n", currentSchema)
//
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 err != nil {
return err
//
if currentSchema == 1 {
log.Println("Upgrading to DB version 2")
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
}
type TitleInfo struct {
TitleID int64
Title string
IsDeleted bool
}
type Article struct {
ID int64
TitleID int64
Modified int64
Body []byte
Author string
Title string
TitleInfo
ArticleID int64
Modified int64
Body []byte
Author string
}
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) {
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)
}
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)
}
@ -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)
}
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 {
isNewArticle := false
a, err := this.GetLatestVersion(title)
@ -114,8 +169,8 @@ func (this *WikiDB) SaveArticle(title, author, body string, expectBaseRev int64)
}
}
if !isNewArticle && a.ID != expectBaseRev {
return ArticleAlteredError{got: expectBaseRev, expected: a.ID}
if !isNewArticle && a.ArticleID != expectBaseRev {
return ArticleAlteredError{got: expectBaseRev, expected: a.ArticleID}
}
zBody, err := gzdeflate([]byte(body), this.compressionLevel)
@ -125,7 +180,7 @@ func (this *WikiDB) SaveArticle(title, author, body string, expectBaseRev int64)
var titleId int64
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 {
return err
}
@ -143,11 +198,21 @@ func (this *WikiDB) SaveArticle(title, author, body string, expectBaseRev int64)
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
}
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 {
return nil, err
}
@ -156,7 +221,7 @@ func (this *WikiDB) GetRevisionHistory(title string) ([]Article, error) {
ret := make([]Article, 0)
for rows.Next() {
a := Article{}
err := rows.Scan(&a.ID, &a.Modified, &a.Author)
err := rows.Scan(&a.ArticleID, &a.Modified, &a.Author)
if err != nil {
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) {
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),
)
if err != nil {
@ -193,7 +258,7 @@ func (this *WikiDB) GetRecentChanges(offset int, limit int) ([]Article, error) {
ret := make([]Article, 0, limit)
for rows.Next() {
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 {
return nil, err
}
@ -213,17 +278,23 @@ func (this *WikiDB) TotalRevisions() (int64, error) {
return ret, nil
}
func (this *WikiDB) ListTitles() ([]string, error) {
rows, err := this.db.Query(`SELECT title FROM titles ORDER BY title ASC`)
func (this *WikiDB) ListTitles(includeDeleted bool) ([]TitleInfo, error) {
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 {
return nil, err
}
defer rows.Close()
ret := make([]string, 0)
ret := make([]TitleInfo, 0)
for rows.Next() {
var title string
err = rows.Scan(&title)
var title TitleInfo
err = rows.Scan(&title.TitleID, &title.Title, &title.IsDeleted)
if err != nil {
return nil, err
}
@ -236,7 +307,7 @@ func (this *WikiDB) ListTitles() ([]string, error) {
func (this *WikiDB) parseArticle(row *sql.Row) (*Article, error) {
a := Article{}
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 {
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) {
a := Article{}
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 {
return nil, err
}

View File

@ -2,20 +2,24 @@
# Makefile for YATWiki3
#
VERSION:=3.1.0
VERSION:=3.3.1
SOURCES:=Makefile \
static \
cmd $(wildcard cmd/yatwiki-server/*.go) \
Gopkg.lock Gopkg.toml \
$(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: all dist clean
.PHONY: all dist clean deps
all: build/linux64/yatwiki-server build/win32/yatwiki-server.exe
@ -25,9 +29,14 @@ dist: \
_dist/yatwiki-$(VERSION)-src.zip
clean:
if [ -f ./staticResources.go ] ; then rm ./staticResources.go ; fi
if [ -d ./build ] ; then rm -r ./build ; fi
if [ -f ./yatwiki ] ; then rm ./yatwiki ; fi
rm -f ./staticResources.go
rm -fr ./build
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

View File

@ -1,10 +1,8 @@
# YATWiki
A semi-anonymous wiki for use in trusted environments.
As of 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.
=FEATURES=
## Features
- Standalone server, easy to run
- Built-in SQLite database
@ -13,28 +11,54 @@ As of the 3.0 release, YATWiki is now a standalone server instead of a PHP scrip
- RSS changelog
- IP-based ban system
- Article index, random article, download database backup
- Source code highlighting (thanks [url=https://github.com/isagalaev/highlight.js]highlight.js[/url])
- Optional integration with `contented` for file/image uploads
- Source code highlighting (thanks [highlight.js](https://github.com/isagalaev/highlight.js) )
- Optional integration with [`contented`](https://code.ivysaur.me/contented/) for file/image uploads
Written in Golang, PHP
For the 20150901 release, a desktop version is available for Windows (based on PHPDesktop).
=USAGE=
Prior to the 3.0 release, YATWiki was a PHP script instead of a standalone server.
## Usage
You can start YATWiki by running the binary. A default configuration file and database will be automatically generated if they are not found.
`Usage of ./yatwiki-server:
```
Usage of ./yatwiki-server:
-config string
Configuration file (default "config.json")
-listen string
Bind address (default "127.0.0.1:80")
`
```
=GO GET=
## Changelog
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]
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
=CHANGELOG=
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
@ -48,7 +72,7 @@ This package can be installed via go get: `go get code.ivysaur.me/yatwiki`
- Fix an issue with `[html]` tags
- Fix an issue with viewing history for unknown articles
2017-07-11 3.0
2017-07-11 3.0.0
- YATWiki was rewritten in Go.
- Enhancement: Standalone binary server
- Enhancement: No longer requires cookies for error messages

View File

@ -22,6 +22,7 @@ type ServerOptions struct {
DeclareRSSLanguage string
DeclareRSSEmail string
ContentedServer string
ContentedBBCodeTag string
}
func DefaultOptions() *ServerOptions {
@ -43,5 +44,6 @@ func DefaultOptions() *ServerOptions {
DeclareRSSLanguage: "en-GB",
DeclareRSSEmail: `nobody@example.com`,
ContentedServer: "",
ContentedBBCodeTag: "",
}
}

View File

@ -14,6 +14,8 @@ import (
"time"
)
var SERVER_HEADER string = "YATWiki/0.0.0-devel"
type WikiServer struct {
db *WikiDB
opts *ServerOptions
@ -74,12 +76,16 @@ func NewWikiServer(opts *ServerOptions) (*WikiServer, error) {
return &ws, nil
}
func (this *WikiServer) GetBBCodeRenderer() *BBCodeRenderer {
return NewBBCodeRenderer(this.opts.ExpectBaseURL, this.opts.ContentedServer, this.opts.ContentedBBCodeTag)
}
func (this *WikiServer) Close() {
this.db.Close()
}
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 {
remoteIP := RemoteAddrToIPAddress(r.RemoteAddr)
@ -110,9 +116,13 @@ func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write(content)
return
} else if remainingPath == "favicon.ico" && len(this.opts.FaviconFilePath) > 0 {
w.Header().Set("Content-Type", "image/x-icon")
http.ServeFile(w, r, this.opts.FaviconFilePath)
} else if remainingPath == "favicon.ico" {
if len(this.opts.FaviconFilePath) > 0 {
w.Header().Set("Content-Type", "image/x-icon")
http.ServeFile(w, r, this.opts.FaviconFilePath)
} else {
http.Error(w, "Not found", 404)
}
return
} else if remainingPath == "download-database" {
@ -138,14 +148,14 @@ func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
} 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 {
this.serveInternalError(w, r, err)
return
}
chosenArticle := titles[rand.Intn(len(titles))]
this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.PathEscape(chosenArticle))
this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.PathEscape(chosenArticle.Title))
return
} else if strings.HasPrefix(remainingPath, "view/") {

View File

@ -2,74 +2,116 @@ package yatwiki
import (
"encoding/json"
"html"
"html/template"
"net/url"
"regexp"
"strings"
)
const (
bbcodeIsDeleted string = `[delete]`
)
// An embarassing cascade of half-working hacks follows.
type BBCodeRenderer struct {
baseUrl string
CodePresent bool
DynamicContentWarning string
ContentedURL string
ContentedTag string
}
func NewBBCodeRenderer(baseUrl string) *BBCodeRenderer {
func NewBBCodeRenderer(baseUrl, ContentedURL, ContentedTag string) *BBCodeRenderer {
return &BBCodeRenderer{
baseUrl: baseUrl,
CodePresent: false,
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 {
match *regexp.Regexp
replace string
*regexp.Regexp
replace string
}
func (this pregReplaceRule) Apply(in string) string {
return this.ReplaceAllString(in, this.replace)
}
type pregReplaceFunc struct {
*regexp.Regexp
replaceFunc func([]string) string
}
func (this pregReplaceFunc) Apply(in string) string {
return PregReplaceCallback(this.Regexp, this.replaceFunc, in)
}
// Internal part of BBCode rendering.
// It handles most leaf-level tags.
func (this *BBCodeRenderer) bbcode(data string) string {
s_to_r := []pregReplaceRule{
pregReplaceRule{regexp.MustCompile(`(?si)\[h\](.*?)\[/h\]`), `<h2>$1</h2>`, nil},
pregReplaceRule{regexp.MustCompile(`(?si)\[b\](.*?)\[/b\]`), `<b>$1</b>`, nil},
pregReplaceRule{regexp.MustCompile(`(?si)\[i\](.*?)\[/i\]`), `<i>$1</i>`, nil},
pregReplaceRule{regexp.MustCompile(`(?si)\[u\](.*?)\[/u\]`), `<u>$1</u>`, nil},
pregReplaceRule{regexp.MustCompile(`(?si)\[s\](.*?)\[/s\]`), `<span class="s">$1</span>`, nil},
pregReplaceRule{regexp.MustCompile(`(?si)\[spoiler\](.*?)\[/spoiler\]`), `<span class="spoiler">$1</span>`, nil},
pregReplaceRule{regexp.MustCompile(`(?si)\[img\](.*?)\[/img\]`), `<img alt="" src="$1">`, nil},
pregReplaceRule{regexp.MustCompile(`(?si)\[list\](.*?)\[/list\]`), `<ul><li>$1</li></ul>`, nil},
pregReplaceRule{regexp.MustCompile(`(?si)\[\*\]`), `</li><li>`, nil},
pregReplaceRule{regexp.MustCompile(`(?si)\[url=(.*?)\](.*?)\[/url\]`), `<a rel="noreferrer" href="$1">$2</a>`, nil},
pregReplaceRule{regexp.MustCompile(`(?si)\[article=(.*?)\](.*?)\[/article\]`), "", func(m []string) string {
return `<a href="` + template.HTMLEscapeString(this.baseUrl+`view/`+url.PathEscape(m[1])) + `">` + m[2] + `</a>`
s_to_r := []transformation{
pregReplaceRule{regexp.MustCompile(`(?si)\[h\](.*?)\[/h\]`), `<h2>$1</h2>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[b\](.*?)\[/b\]`), `<b>$1</b>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[i\](.*?)\[/i\]`), `<i>$1</i>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[u\](.*?)\[/u\]`), `<u>$1</u>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[s\](.*?)\[/s\]`), `<span class="s">$1</span>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[spoiler\](.*?)\[/spoiler\]`), `<span class="spoiler">$1</span>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[img\](.*?)\[/img\]`), `<img alt="" src="$1">`},
pregReplaceRule{regexp.MustCompile(`(?si)\[list\](.*?)\[/list\]`), `<ul><li>$1</li></ul>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[\*\]`), `</li><li>`},
pregReplaceRule{regexp.MustCompile(`(?si)\[url=(.*?)\](.*?)\[/url\]`), `<a rel="noreferrer" href="$1">$2</a>`},
pregReplaceFunc{regexp.MustCompile(`(?si)\[article=(.*?)\](.*?)\[/article\]`), func(m []string) string {
// 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.PathEscape(m[1])) + `">` + m[2] + `</a>`
}},
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>`,
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>`
}},
}
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.match.MatchString(data) { // repeat until all recursive replacements are consumed
if len(prr.replace) > 0 {
data = prr.match.ReplaceAllString(data, prr.replace)
} else {
data = PregReplaceCallback(prr.match, prr.replaceFunc, data)
}
for prr.MatchString(data) { // repeat until all recursive replacements are consumed
data = prr.Apply(data)
}
}
return data

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,8 +3,8 @@ package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
@ -19,43 +19,40 @@ func main() {
opts := yatwiki.ServerOptions{}
cfg, err := ioutil.ReadFile(*configPath)
if err != nil {
if os.IsNotExist(err) {
opts = *yatwiki.DefaultOptions()
if cfg, err := json.MarshalIndent(opts, "", "\t"); err == nil {
err := ioutil.WriteFile(*configPath, cfg, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to save default configuration file: %s", err.Error())
}
// Create default configuration file if necessary
if _, err := os.Stat(*configPath); os.IsNotExist(err) {
log.Printf("Creating default configuration file at '%s'...\n", *configPath)
opts = *yatwiki.DefaultOptions()
if cfg, err := json.MarshalIndent(opts, "", "\t"); err == nil {
err := ioutil.WriteFile(*configPath, cfg, 0644)
if err != nil {
log.Printf("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)
}
}
// Load configuration
log.Printf("Loading configuration from '%s'...\n", *configPath)
cfg, err := ioutil.ReadFile(*configPath)
if err != nil {
log.Fatalf("Failed to load configuration file '%s': %s\n", *configPath, err.Error())
}
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 {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
log.Fatalln(err.Error())
}
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)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
log.Fatalln(err.Error())
}
os.Exit(0)
}

View File

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

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module code.ivysaur.me/yatwiki
require github.com/mattn/go-sqlite3 v1.6.0

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
github.com/mattn/go-sqlite3 v1.6.0 h1:TDwTWbeII+88Qy55nWlof0DclgAtI4LqGujkYMzmQII=
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=

View File

@ -11,6 +11,8 @@ var subresourceNonce = time.Now().Unix()
type pageTemplateOptions struct {
CurrentPageIsArticle bool
CurrentPageName string
CurrentPageIsRev bool
CurrentPageRev int64
WikiTitle string
Content template.HTML
BaseURL string
@ -44,7 +46,7 @@ const pageTemplate string = `<!DOCTYPE html>
<head>
<title>{{.CurrentPageName}}{{ if len .CurrentPageName }} - {{end}}{{.WikiTitle}}</title>
<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 http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="alternate" type="application/rss+xml" href="{{.BaseURL}}rss/changes" title="{{.WikiTitle}} - Recent Changes">
@ -87,23 +89,54 @@ function els(e,s){ // no js exec in innerHTML
</head>
<body>
<div class="header">
<a href="{{.BaseURL}}view/{{.DefaultPage | pathcomponent}}" 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 | pathcomponent}}" title="New Page"><div class="sprite nw"></div></a>
<a href="{{.BaseURL}}view/{{.DefaultPage | pathcomponent}}" title="Home"><div class="sprite">
<svg viewBox="0 0 24 24">
<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 }}
<div class="sep"></div>
<a href="{{.BaseURL}}history/{{.CurrentPageName | pathcomponent}}" title="Page History"><div class="sprite hs"></div></a>
<a href="{{.BaseURL}}modify/{{.CurrentPageName | pathcomponent}}" title="Modify Page"><div class="sprite ed"></div></a>
<a href="{{.BaseURL}}history/{{.CurrentPageName | pathcomponent}}" title="Page History"><div class="sprite">
<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}}
</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/1"><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>
<a href="{{.BaseURL}}recent/1"><div class="sprite"></div> Recent Changes</a>
<a href="{{.BaseURL}}random"><div class="sprite">
<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}}
<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}}
</div>
<div class="content">

View File

@ -25,13 +25,18 @@ func (this *WikiServer) routeArchive(w http.ResponseWriter, r *http.Request, rev
pto.CurrentPageName = a.Title
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(
`<div class="info">`+
`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.PathEscape(a.Title))+`">here</a> to see the latest revision.`+
`</div>`,
`<div class="info">`+infoMessageHtml+`</div>`,
) + bcr.RenderHTML(string(a.Body))
pto.LoadCodeResources = bcr.CodePresent

View File

@ -53,9 +53,9 @@ func (this *WikiServer) routeDiff(w http.ResponseWriter, r *http.Request, oldRev
pto.Content = template.HTML(
`<h2>` +
`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>` +
`<pre>` + string(b.Bytes()) + `</pre>`,
)

View File

@ -17,6 +17,10 @@ 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)
}
func (this *WikiServer) serveDeleted(w http.ResponseWriter, lookingFor string) {
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))
}
@ -47,9 +51,11 @@ 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
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 {

View File

@ -1,13 +1,15 @@
package yatwiki
import (
"html/template"
"net/http"
)
func (this *WikiServer) routeFormatting(w http.ResponseWriter, r *http.Request) {
pto := DefaultPageTemplateOptions(this.opts)
pto.CurrentPageName = "Formatting help"
pto.Content = `
content := `
<h2>Formatting help</h2><br><br>
<ul>
<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>[img]image-url[/img]</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>[section=header]content[/section]</li>
<li>[youtube]id[/youtube]</li>
<li>[html]raw html[/html]</li>
<li>` + bbcodeIsDeleted + `</li>
</ul>`
pto.Content = template.HTML(content)
this.servePageResponse(w, r, pto)
}

View File

@ -37,9 +37,9 @@ func (this *WikiServer) routeHistory(w http.ResponseWriter, r *http.Request, art
compareRow := `<tr><td colspan="2"></td><td><input type="submit" value="Compare Selected &raquo;"></td></tr>`
content += compareRow
for _, rev := range revs {
revIdStr := fmt.Sprintf("%d", rev.ID)
revIdStr := fmt.Sprintf("%d", rev.ArticleID)
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><input type="radio" name="t" value="` + revIdStr + `">&nbsp;<input type="radio" name="f" value="` + revIdStr + `"></td>` +
`</tr>`

View File

@ -8,12 +8,15 @@ import (
)
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 {
this.serveInternalError(w, r, err)
return
}
showDeleted := (r.FormValue("deleted") == "1")
anyDeleted := false
totalRevs, err := this.db.TotalRevisions()
if err != nil {
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))
for _, title := range titles {
content += `<li><a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(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>`
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.CurrentPageName = "Index"
pto.Content = template.HTML(content)

View File

@ -33,14 +33,14 @@ func (this *WikiServer) routeModify(w http.ResponseWriter, r *http.Request, arti
baseRev = 0
} else {
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)
}
content := `
<h2>` + pageTitleHTML + `</h2><br>
<form method="POST" action="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`save`) + `" class="editor" accept-charset="UTF-8">
<form method="POST" action="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`save`) + `" class="editor" accept-charset="UTF-8" id="form-edit-page">
<div class="frm">
<label>
Save as:
@ -52,14 +52,13 @@ func (this *WikiServer) routeModify(w http.ResponseWriter, r *http.Request, arti
`
if len(this.opts.ContentedServer) > 0 {
content += `
<script type="text/javascript" src="` + this.opts.ContentedServer + `jquery-1.12.4.min.js"></script>
<script type="text/javascript" src="` + this.opts.ContentedServer + `sdk.js"></script>
| <a href="javascript:;" id="open-contented-uploader">upload...</a>
<script type="text/javascript">
$("#open-contented-uploader").on('click', function() {
document.getElementById("open-contented-uploader").addEventListener("click", function() {
contented.init("#contentctr", function(items) {
for (var i = 0; i < items.length; ++i) {
$("#contentctr textarea").append(" " + "` + this.opts.ContentedServer + `get/" + items[i] + " ");
$("#contentctr textarea").append(" " + contented.getPreviewURL(items[i]) + " ");
}
});
});
@ -68,6 +67,16 @@ func (this *WikiServer) routeModify(w http.ResponseWriter, r *http.Request, arti
}
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 id="contentctr"><textarea name="content">` + template.HTMLEscapeString(existingBody) + `</textarea></div>
</form>

10
rRSS.go
View File

@ -27,17 +27,17 @@ func (this *WikiServer) routeRecentChangesRSS(w http.ResponseWriter, r *http.Req
for _, a := range recents {
content += `
<item>
<title>` + template.HTMLEscapeString(a.Title+` (r`+fmt.Sprintf("%d", a.ID)+`)`) + `</title>
<link>` + template.HTMLEscapeString(this.opts.ExternalBaseURL+`archive/`+fmt.Sprintf("%d", a.ID)) + `</link>
<guid>` + template.HTMLEscapeString(this.opts.ExternalBaseURL+`archive/`+fmt.Sprintf("%d", a.ID)) + `</guid>
<title>` + template.HTMLEscapeString(a.Title+` (r`+fmt.Sprintf("%d", a.ArticleID)+`)`) + `</title>
<link>` + template.HTMLEscapeString(this.opts.ExternalBaseURL+`archive/`+fmt.Sprintf("%d", a.ArticleID)) + `</link>
<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>
<pubDate>` + template.HTMLEscapeString(time.Unix(a.Modified, 0).In(this.loc).Format(time.RFC1123Z)) + `</pubDate>
<description>` + template.HTMLEscapeString(`
<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>
</item>
`

View File

@ -37,25 +37,46 @@ func (this *WikiServer) routeRecentChanges(w http.ResponseWriter, r *http.Reques
pto.CurrentPageName = "Recent Changes"
content := `<h2>Recent Changes</h2><br>` +
`<em>Showing up to ` + fmt.Sprintf("%d", this.opts.RecentChanges) + ` changes.</em><br>` +
`<table>`
`<em>Showing up to ` + fmt.Sprintf("%d", this.opts.RecentChanges) + ` changes.</em><br><br>` +
`<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 {
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>` +
`<td><a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(rev.Title)) + `">` + template.HTMLEscapeString(rev.Title) + `</a>` +
` [<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", rev.ID)) + `">a</a>]` +
`<td><a ` + classAttr + ` href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(rev.Title)) + `">` + template.HTMLEscapeString(rev.Title) + `</a></td>` +
`<td>` +
`<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", rev.ArticleID)) + `">rev</a> &nbsp; ` +
diffHtml +
`</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>`
}
content += `<tr><td>`
content += `</table>`
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 {
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)
this.servePageResponse(w, r, pto)

View File

@ -26,11 +26,16 @@ func (this *WikiServer) routeView(w http.ResponseWriter, r *http.Request, articl
return
}
if string(a.Body) == bbcodeIsDeleted {
this.serveDeleted(w, articleTitle)
return
}
pto := DefaultPageTemplateOptions(this.opts)
pto.CurrentPageName = articleTitle
pto.CurrentPageIsArticle = true
bcr := NewBBCodeRenderer(this.opts.ExpectBaseURL)
bcr := this.GetBBCodeRenderer()
pto.Content = bcr.RenderHTML(string(a.Body))
pto.LoadCodeResources = bcr.CodePresent

View File

@ -18,6 +18,11 @@ td {padding:0px 10px;}
.spoiler{color:black;background-color:black;}
.spoiler:hover{color:white;}
a.deleted {
color:red;
text-decoration:line-through;
}
.imgur {
border:1px solid white;
width:90px;
@ -112,16 +117,6 @@ fieldset legend {
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 {

File diff suppressed because one or more lines are too long