29 Commits

Author SHA1 Message Date
6c40ab1512 doc/README: changelog for v3.3.1 2025-08-20 16:33:26 +12:00
a8c60ff89c deps: update deps, declare golang language version 2025-08-20 16:33:19 +12:00
2faaf3ca56 makefile: remove obsolete makefile 2025-08-20 16:33:08 +12:00
15b6473b06 static: replace go-bindata usage with standard go:embed 2025-08-20 16:32:49 +12:00
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
24 changed files with 354 additions and 525 deletions

147
DB.go
View File

@@ -3,6 +3,7 @@ package yatwiki
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"log"
"strings" "strings"
"time" "time"
@@ -33,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) {
@@ -87,7 +137,7 @@ 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)
} }
@@ -119,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)
@@ -148,6 +198,13 @@ 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
} }
@@ -164,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
} }
@@ -190,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 {
@@ -201,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
} }
@@ -221,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
} }
@@ -244,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
} }
@@ -262,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
} }

21
Gopkg.lock generated
View File

@@ -1,21 +0,0 @@
# 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 = "5160b48509cf5c877bc22c11c373f8c7738cdb38"
version = "v1.3.0"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = ["context"]
revision = "c73622c77280266305273cb545f54516ced95b93"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "a1f2d643f8c1770c92ee1759184a0c7004af5672869db579328d05bb7cfd6bef"
solver-name = "gps-cdcl"
solver-version = 1

View File

@@ -1,26 +0,0 @@
# 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

@@ -1,82 +0,0 @@
#
# Makefile for YATWiki3
#
VERSION:=3.2.0
SOURCES:=Makefile \
static \
cmd $(wildcard cmd/yatwiki-server/*.go) \
Gopkg.lock Gopkg.toml \
$(wildcard *.go)
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 deps
all: build/linux64/yatwiki-server build/win32/yatwiki-server.exe
dist: \
_dist/yatwiki-$(VERSION)-linux64.tar.gz \
_dist/yatwiki-$(VERSION)-win32.7z \
_dist/yatwiki-$(VERSION)-src.zip
clean:
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
#
staticResources.go: static/ static/*
go-bindata -o staticResources.go -prefix static -pkg yatwiki static
#
# Release artefacts
#
build/linux64/yatwiki-server: $(SOURCES) staticResources.go
mkdir -p build/linux64
(cd cmd/yatwiki-server ; \
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
go build $(GOFLAGS) -o ../../build/linux64/yatwiki-server \
)
build/win32/yatwiki-server.exe: $(SOURCES) staticResources.go
mkdir -p build/win32
(cd cmd/yatwiki-server ; \
PATH=/usr/lib/mxe/usr/bin:$(PATH) CC=i686-w64-mingw32.static-gcc \
CGO_ENABLED=1 GOOS=windows GOARCH=386 \
go build $(GOFLAGS) -o ../../build/win32/yatwiki-server.exe \
)
_dist/yatwiki-$(VERSION)-linux64.tar.gz: build/linux64/yatwiki-server
mkdir -p _dist
tar caf _dist/yatwiki-$(VERSION)-linux64.tar.gz -C build/linux64 yatwiki-server --owner=0 --group=0
_dist/yatwiki-$(VERSION)-win32.7z: build/win32/yatwiki-server.exe
mkdir -p _dist
( cd build/win32 ; \
if [ -f dist.7z ] ; then rm dist.7z ; fi ; \
7z a dist.7z yatwiki-server.exe ; \
mv dist.7z ../../_dist/yatwiki-$(VERSION)-win32.7z \
)
_dist/yatwiki-$(VERSION)-src.zip: $(SOURCES)
git archive --format=zip HEAD > _dist/yatwiki-$(VERSION)-src.zip

View File

@@ -1,10 +1,8 @@
# YATWiki
A semi-anonymous wiki for use in trusted environments. A semi-anonymous wiki for use in trusted environments.
For the 20150901 release, a desktop version is available for Windows (based on PHPDesktop). ## Features
As of the 3.0 release, YATWiki is now a standalone server instead of a PHP script.
=FEATURES=
- Standalone server, easy to run - Standalone server, easy to run
- Built-in SQLite database - Built-in SQLite database
@@ -13,28 +11,41 @@ As of the 3.0 release, YATWiki is now a standalone server instead of a PHP scrip
- RSS changelog - RSS changelog
- 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 [highlight.js](https://github.com/isagalaev/highlight.js) )
- Optional integration with `contented` for file/image uploads - 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. 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 -config string
Configuration file (default "config.json") Configuration file (default "config.json")
-listen string -listen string
Bind address (default "127.0.0.1:80") 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` 2025-08-20 3.3.1
[go-get]code.ivysaur.me/yatwiki git https://git.ivysaur.me/code.ivysaur.me/yatwiki.git[/go-get] - Update dependencies, replace go-bindata with `go:embed`
=CHANGELOG= 2018-12-31 (no release)
- Convert to Go Modules
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 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: Add new ContentedBBCodeTag option to choose a BBCode tag for mini thumbnails (requires `contented` >= 1.2.0)
@@ -67,7 +78,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 `[html]` tags
- Fix an issue with viewing history for unknown articles - 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. - 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

View File

@@ -2,6 +2,7 @@ package yatwiki
import ( import (
"database/sql" "database/sql"
"embed"
"errors" "errors"
"fmt" "fmt"
"html/template" "html/template"
@@ -16,6 +17,9 @@ import (
var SERVER_HEADER string = "YATWiki/0.0.0-devel" var SERVER_HEADER string = "YATWiki/0.0.0-devel"
//go:embed static/*
var staticResources embed.FS
type WikiServer struct { type WikiServer struct {
db *WikiDB db *WikiDB
opts *ServerOptions opts *ServerOptions
@@ -106,13 +110,13 @@ func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" { if r.Method == "GET" {
if remainingPath == "wiki.css" { if remainingPath == "wiki.css" {
w.Header().Set("Content-Type", "text/css") w.Header().Set("Content-Type", "text/css")
content, _ := wikiCssBytes() content, _ := staticResources.ReadFile(`static/wiki.css`)
w.Write(content) w.Write(content)
return return
} else if remainingPath == "highlight.js" { } else if remainingPath == "highlight.js" {
w.Header().Set("Content-Type", "application/javascript") w.Header().Set("Content-Type", "application/javascript")
content, _ := highlightJsBytes() content, _ := staticResources.ReadFile(`static/highlight.js`)
w.Write(content) w.Write(content)
return return
@@ -148,14 +152,14 @@ func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
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.PathEscape(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/") {

View File

@@ -9,6 +9,10 @@ import (
"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
@@ -28,40 +32,70 @@ func NewBBCodeRenderer(baseUrl, ContentedURL, ContentedTag string) *BBCodeRender
} }
} }
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.PathEscape(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.PathEscape(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>`
}}, }},
} }
@@ -70,20 +104,14 @@ func (this *BBCodeRenderer) bbcode(data string) string {
s_to_r = append(s_to_r, s_to_r = append(s_to_r,
pregReplaceRule{regexp.MustCompile(`(?si)\[` + regexp.QuoteMeta(this.ContentedTag) + `\](.*?)\[/` + regexp.QuoteMeta(this.ContentedTag) + `\]`), 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>`, `<a href="` + html.EscapeString(this.ContentedURL) + `p/${1}"><img class="imgur" alt="" src="` + html.EscapeString(this.ContentedURL) + `thumb/s/${1}" ></a>`,
nil,
}, },
) )
} }
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

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 ( import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"os" "os"
@@ -19,43 +19,40 @@ func main() {
opts := yatwiki.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 = *yatwiki.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)
} }
} }
// 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) ws, err := yatwiki.NewWikiServer(&opts)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err.Error()) log.Fatalln(err.Error())
os.Exit(1)
} }
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)
} }

5
go.mod Normal file
View File

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

2
go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=

View File

@@ -11,6 +11,8 @@ 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
@@ -44,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">
@@ -114,6 +116,13 @@ function els(e,s){ // no js exec in innerHTML
<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" /> <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> </svg>
</div></a> </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>

View File

@@ -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
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() 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.PathEscape(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

@@ -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

@@ -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) 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) { 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)) this.serveRedirect(w, this.opts.ExpectBaseURL+"view/"+url.PathEscape(this.opts.DefaultPage)+"?error="+url.QueryEscape(msg))
} }

View File

@@ -33,7 +33,9 @@ func (this *WikiServer) routeFormatting(w http.ResponseWriter, r *http.Request)
content += ` 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) pto.Content = template.HTML(content)

View File

@@ -37,7 +37,7 @@ 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>` 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) + `">` + string(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>` +

View File

@@ -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.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>` 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

@@ -33,14 +33,14 @@ func (this *WikiServer) routeModify(w http.ResponseWriter, r *http.Request, arti
baseRev = 0 baseRev = 0
} else { } else {
pageTitleHTML = `Editing article &quot;<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(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)
} }
content := ` content := `
<h2>` + pageTitleHTML + `</h2><br> <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"> <div class="frm">
<label> <label>
Save as: Save as:
@@ -67,6 +67,16 @@ func (this *WikiServer) routeModify(w http.ResponseWriter, r *http.Request, arti
} }
content += ` 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>

10
rRSS.go
View File

@@ -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.PathEscape(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

@@ -44,17 +44,22 @@ func (this *WikiServer) routeRecentChanges(w http.ResponseWriter, r *http.Reques
for _, rev := range recents { for _, rev := range recents {
diffHtml := "" diffHtml := ""
diffRev, err := this.db.GetNextOldestRevision(int(rev.ID)) diffRev, err := this.db.GetNextOldestRevision(int(rev.ArticleID))
if err != nil { if err != nil {
diffHtml = `[new]` diffHtml = `[new]`
} else { } else {
diffHtml = `<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`diff/`+fmt.Sprintf("%d/%d", diffRev, rev.ID)) + `">diff</a>` 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.PathEscape(rev.Title)) + `">` + template.HTMLEscapeString(rev.Title) + `</a></td>` + `<td><a ` + classAttr + ` href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(rev.Title)) + `">` + template.HTMLEscapeString(rev.Title) + `</a></td>` +
`<td>` + `<td>` +
`<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", rev.ID)) + `">rev</a> &nbsp; ` + `<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", rev.ArticleID)) + `">rev</a> &nbsp; ` +
diffHtml + diffHtml +
`</td>` + `</td>` +
`</td>` + `</td>` +

View File

@@ -26,6 +26,11 @@ func (this *WikiServer) routeView(w http.ResponseWriter, r *http.Request, articl
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

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;

File diff suppressed because one or more lines are too long