Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c40ab1512 | |||
| a8c60ff89c | |||
| 2faaf3ca56 | |||
| 15b6473b06 | |||
| 206ea45115 | |||
| f2a69d1ed7 | |||
| 7a6456aecc | |||
| e44607b429 | |||
| 7b268108da | |||
| 4f12dd0564 | |||
| 0fb52800f7 | |||
| b080a6f017 | |||
| ec116a09b8 | |||
| 5148c59944 | |||
| 0a332642d7 | |||
| 6e63285a54 | |||
| 2c5f1e9244 | |||
| 6a56a4b1b2 | |||
| 6c70f37ef8 | |||
| eeb2308c54 | |||
| 96b5318eca | |||
| 9aa80bf772 | |||
| adbe71525a | |||
| 65e43df8d2 | |||
| ee4ed51530 | |||
| 91887bcee5 | |||
| 993bd6c4f3 | |||
| e0e30372ef | |||
| 6b41df964b |
147
DB.go
147
DB.go
@@ -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(¤tSchema); 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
21
Gopkg.lock
generated
@@ -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
|
|
||||||
26
Gopkg.toml
26
Gopkg.toml
@@ -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"
|
|
||||||
82
Makefile
82
Makefile
@@ -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
|
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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/") {
|
||||||
|
|||||||
80
bbcode.go
80
bbcode.go
@@ -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
41
bbcode_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
5
go.mod
Normal 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
2
go.sum
Normal 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=
|
||||||
@@ -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>
|
||||||
|
|||||||
15
rArchive.go
15
rArchive.go
@@ -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
|
||||||
|
|
||||||
|
|||||||
4
rDiff.go
4
rDiff.go
@@ -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>`,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 »"></td></tr>`
|
compareRow := `<tr><td colspan="2"></td><td><input type="submit" value="Compare Selected »"></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>` +
|
||||||
|
|||||||
24
rIndex.go
24
rIndex.go
@@ -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)
|
||||||
|
|||||||
14
rModify.go
14
rModify.go
@@ -33,14 +33,14 @@ func (this *WikiServer) routeModify(w http.ResponseWriter, r *http.Request, arti
|
|||||||
baseRev = 0
|
baseRev = 0
|
||||||
} else {
|
} else {
|
||||||
pageTitleHTML = `Editing article "<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(articleTitle)) + `">` + template.HTMLEscapeString(articleTitle) + `</a>"`
|
pageTitleHTML = `Editing article "<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(articleTitle)) + `">` + template.HTMLEscapeString(articleTitle) + `</a>"`
|
||||||
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
10
rRSS.go
@@ -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>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -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> ` +
|
`<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", rev.ArticleID)) + `">rev</a> ` +
|
||||||
diffHtml +
|
diffHtml +
|
||||||
`</td>` +
|
`</td>` +
|
||||||
`</td>` +
|
`</td>` +
|
||||||
|
|||||||
5
rView.go
5
rView.go
@@ -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
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user