Compare commits
	
		
			64 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7b268108da | |||
| 4f12dd0564 | |||
| 0fb52800f7 | |||
| b080a6f017 | |||
| ec116a09b8 | |||
| 5148c59944 | |||
| 0a332642d7 | |||
| 6e63285a54 | |||
| 2c5f1e9244 | |||
| 6a56a4b1b2 | |||
| 6c70f37ef8 | |||
| eeb2308c54 | |||
| 96b5318eca | |||
| 9aa80bf772 | |||
| adbe71525a | |||
| 65e43df8d2 | |||
| ee4ed51530 | |||
| 91887bcee5 | |||
| 993bd6c4f3 | |||
| e0e30372ef | |||
| 6b41df964b | |||
| db659236bf | |||
| 817b1690e7 | |||
| 5b533f7b40 | |||
| 51aae382b7 | |||
| cfc0107bef | |||
| e997e1b08a | |||
| c830c2b4dd | |||
| fdb854e6c7 | |||
| f934c2917f | |||
| 1bfefdccb3 | |||
| 9ca58bc16c | |||
| 122acf6999 | |||
| f627946c0d | |||
| 5b42685956 | |||
| 90fedf86d9 | |||
| a9a6b51a3f | |||
| 9687f90cf5 | |||
| 5cc93387e7 | |||
| fc57e4d8f3 | |||
| 2bc26c5966 | |||
| f5767db840 | |||
| edf88d1f31 | |||
| 262c3ba903 | |||
| a260d102ee | |||
| 179617d058 | |||
| 5347efb51a | |||
| e3cee5b94c | |||
| e4cf02cde7 | |||
| 06e5b4ddf9 | |||
| dbf5e1b246 | |||
| fbad854279 | |||
| d78429129f | |||
| d937ea6562 | |||
| 884acd3040 | |||
| e515d73052 | |||
| c87eaa637a | |||
| 043720a086 | |||
| a12af6967c | |||
| 9792c262a2 | |||
| 6d9079e1dc | |||
| 1c4505a2d9 | |||
| 11a4f97212 | |||
| 74b3124997 | 
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -6,5 +6,8 @@ build/
 | 
				
			|||||||
cmd/yatwiki-server/yatwiki-server
 | 
					cmd/yatwiki-server/yatwiki-server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Development db files
 | 
					# Development db files
 | 
				
			||||||
cmd/yatwiki-server/wiki.db
 | 
					cmd/yatwiki-server/*.db
 | 
				
			||||||
cmd/yatwiki-server/config.json
 | 
					cmd/yatwiki-server/config.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Vendor
 | 
				
			||||||
 | 
					vendor/
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"crypto/md5"
 | 
						"crypto/md5"
 | 
				
			||||||
@@ -11,8 +11,16 @@ func RemoteAddrToIPAddress(remoteAddr string) string {
 | 
				
			|||||||
	return strings.TrimRight(strings.TrimRight(remoteAddr, `0123456789`), `:`) // trim trailing port; IPv4 and IPv6-safe
 | 
						return strings.TrimRight(strings.TrimRight(remoteAddr, `0123456789`), `:`) // trim trailing port; IPv4 and IPv6-safe
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func Author(r *http.Request) string {
 | 
					func Author(r *http.Request, trustXForwardedFor bool) string {
 | 
				
			||||||
	userAgentHash := md5.Sum([]byte(r.UserAgent()))
 | 
						userAgentHash := md5.Sum([]byte(r.UserAgent()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return RemoteAddrToIPAddress(r.RemoteAddr) + "-" + hex.EncodeToString(userAgentHash[:])[:6]
 | 
						ipAddress := RemoteAddrToIPAddress(r.RemoteAddr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if trustXForwardedFor {
 | 
				
			||||||
 | 
							if xff := r.Header.Get("X-Forwarded-For"); len(xff) > 0 {
 | 
				
			||||||
 | 
								ipAddress = xff
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ipAddress + "-" + hex.EncodeToString(userAgentHash[:])[:6]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										163
									
								
								DB.go
									
									
									
									
									
								
							
							
						
						
									
										163
									
								
								DB.go
									
									
									
									
									
								
							@@ -1,8 +1,10 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"database/sql"
 | 
						"database/sql"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	_ "github.com/mattn/go-sqlite3"
 | 
						_ "github.com/mattn/go-sqlite3"
 | 
				
			||||||
@@ -32,52 +34,101 @@ func NewWikiDB(dbFilePath string, compressionLevel int) (*WikiDB, error) {
 | 
				
			|||||||
	return &wdb, nil
 | 
						return &wdb, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (this *WikiDB) multiTx(stmts ...string) (err error) {
 | 
				
			||||||
 | 
						tx, err := this.db.Begin()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, stmt := range stmts {
 | 
				
			||||||
 | 
							_, err := tx.Exec(stmt)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								tx.Rollback()
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return tx.Commit()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiDB) assertSchema() error {
 | 
					func (this *WikiDB) assertSchema() error {
 | 
				
			||||||
	_, err := this.db.Exec(`
 | 
						_, err := this.db.Exec(`
 | 
				
			||||||
	CREATE TABLE IF NOT EXISTS articles (
 | 
						CREATE TABLE IF NOT EXISTS schema (
 | 
				
			||||||
		id			INTEGER PRIMARY KEY,
 | 
							id INTEGER PRIMARY KEY
 | 
				
			||||||
		article		INTEGER,
 | 
					 | 
				
			||||||
		modified	INTEGER,
 | 
					 | 
				
			||||||
		body		BLOB,
 | 
					 | 
				
			||||||
		author		TEXT
 | 
					 | 
				
			||||||
	);`)
 | 
						);`)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	_, err = this.db.Exec(`
 | 
						// Look up current value from schema table
 | 
				
			||||||
	CREATE TABLE IF NOT EXISTS titles (
 | 
						schemaLookup := this.db.QueryRow(`SELECT MAX(id) mid FROM schema`)
 | 
				
			||||||
		id INTEGER PRIMARY KEY,
 | 
						currentSchema := int64(0)
 | 
				
			||||||
		title TEXT
 | 
						if err = schemaLookup.Scan(¤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) {
 | 
				
			||||||
@@ -86,12 +137,12 @@ func (this *WikiDB) GetArticleById(articleId int) (*Article, error) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiDB) GetRevision(revId int) (*Article, error) {
 | 
					func (this *WikiDB) GetRevision(revId int) (*Article, error) {
 | 
				
			||||||
	row := this.db.QueryRow(`SELECT articles.*, titles.title FROM articles JOIN titles ON articles.article=titles.id WHERE articles.id = ?`, revId)
 | 
						row := this.db.QueryRow(`SELECT articles.*, titles.title, titles.is_deleted FROM articles JOIN titles ON articles.article=titles.id WHERE articles.id = ?`, revId)
 | 
				
			||||||
	return this.parseArticleWithTitle(row)
 | 
						return this.parseArticleWithTitle(row)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiDB) GetLatestVersion(title string) (*Article, error) {
 | 
					func (this *WikiDB) GetLatestVersion(title string) (*Article, error) {
 | 
				
			||||||
	row := this.db.QueryRow(`SELECT articles.* FROM articles WHERE article = (SELECT id FROM titles WHERE title = ?) ORDER BY modified DESC LIMIT 1`, title)
 | 
						row := this.db.QueryRow(`SELECT articles.* FROM articles WHERE article = (SELECT id FROM titles WHERE title = ?) ORDER BY modified DESC LIMIT 1`, this.normaliseTitle(title))
 | 
				
			||||||
	return this.parseArticle(row)
 | 
						return this.parseArticle(row)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -103,6 +154,10 @@ func (aae ArticleAlteredError) Error() string {
 | 
				
			|||||||
	return fmt.Sprintf("Warning: Your changes were not based on the most recent version of the page (r%d ≠ r%d). No changes were saved.", aae.got, aae.expected)
 | 
						return fmt.Sprintf("Warning: Your changes were not based on the most recent version of the page (r%d ≠ r%d). No changes were saved.", aae.got, aae.expected)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (this *WikiDB) normaliseTitle(title string) string {
 | 
				
			||||||
 | 
						return strings.ToLower(strings.Trim(title, " \r\n\t"))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiDB) SaveArticle(title, author, body string, expectBaseRev int64) error {
 | 
					func (this *WikiDB) SaveArticle(title, author, body string, expectBaseRev int64) error {
 | 
				
			||||||
	isNewArticle := false
 | 
						isNewArticle := false
 | 
				
			||||||
	a, err := this.GetLatestVersion(title)
 | 
						a, err := this.GetLatestVersion(title)
 | 
				
			||||||
@@ -114,8 +169,8 @@ func (this *WikiDB) SaveArticle(title, author, body string, expectBaseRev int64)
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !isNewArticle && a.ID != expectBaseRev {
 | 
						if !isNewArticle && a.ArticleID != expectBaseRev {
 | 
				
			||||||
		return ArticleAlteredError{got: expectBaseRev, expected: a.ID}
 | 
							return ArticleAlteredError{got: expectBaseRev, expected: a.ArticleID}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	zBody, err := gzdeflate([]byte(body), this.compressionLevel)
 | 
						zBody, err := gzdeflate([]byte(body), this.compressionLevel)
 | 
				
			||||||
@@ -125,7 +180,7 @@ func (this *WikiDB) SaveArticle(title, author, body string, expectBaseRev int64)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	var titleId int64
 | 
						var titleId int64
 | 
				
			||||||
	if isNewArticle {
 | 
						if isNewArticle {
 | 
				
			||||||
		titleInsert, err := this.db.Exec(`INSERT INTO titles (title) VALUES (?)`, title)
 | 
							titleInsert, err := this.db.Exec(`INSERT INTO titles (title) VALUES (?)`, this.normaliseTitle(title))
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -143,11 +198,21 @@ func (this *WikiDB) SaveArticle(title, author, body string, expectBaseRev int64)
 | 
				
			|||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Update is-deleted flag
 | 
				
			||||||
 | 
						isDeleted := 0
 | 
				
			||||||
 | 
						if body == bbcodeIsDeleted {
 | 
				
			||||||
 | 
							isDeleted = 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_, err = this.db.Exec(`UPDATE titles SET is_deleted = ? WHERE id = ?`, isDeleted, titleId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiDB) GetRevisionHistory(title string) ([]Article, error) {
 | 
					func (this *WikiDB) GetRevisionHistory(title string) ([]Article, error) {
 | 
				
			||||||
	rows, err := this.db.Query(`SELECT articles.id, articles.modified, articles.author FROM articles WHERE article = (SELECT id FROM titles WHERE title = ?) ORDER BY modified DESC`, title)
 | 
						rows, err := this.db.Query(
 | 
				
			||||||
 | 
							`SELECT articles.id, articles.modified, articles.author FROM articles WHERE article = (SELECT id FROM titles WHERE title = ?) ORDER BY modified DESC`,
 | 
				
			||||||
 | 
							this.normaliseTitle(title),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -156,7 +221,7 @@ func (this *WikiDB) GetRevisionHistory(title string) ([]Article, error) {
 | 
				
			|||||||
	ret := make([]Article, 0)
 | 
						ret := make([]Article, 0)
 | 
				
			||||||
	for rows.Next() {
 | 
						for rows.Next() {
 | 
				
			||||||
		a := Article{}
 | 
							a := Article{}
 | 
				
			||||||
		err := rows.Scan(&a.ID, &a.Modified, &a.Author)
 | 
							err := rows.Scan(&a.ArticleID, &a.Modified, &a.Author)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -182,7 +247,7 @@ func (this *WikiDB) GetNextOldestRevision(revision int) (int, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (this *WikiDB) GetRecentChanges(offset int, limit int) ([]Article, error) {
 | 
					func (this *WikiDB) GetRecentChanges(offset int, limit int) ([]Article, error) {
 | 
				
			||||||
	rows, err := this.db.Query(
 | 
						rows, err := this.db.Query(
 | 
				
			||||||
		`SELECT articles.id, articles.modified, articles.author, titles.title FROM articles JOIN titles ON articles.article=titles.id ORDER BY modified DESC ` +
 | 
							`SELECT articles.id, articles.modified, articles.author, titles.title, titles.is_deleted FROM articles JOIN titles ON articles.article=titles.id ORDER BY modified DESC ` +
 | 
				
			||||||
			fmt.Sprintf(`LIMIT %d OFFSET %d`, limit, offset),
 | 
								fmt.Sprintf(`LIMIT %d OFFSET %d`, limit, offset),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -193,7 +258,7 @@ func (this *WikiDB) GetRecentChanges(offset int, limit int) ([]Article, error) {
 | 
				
			|||||||
	ret := make([]Article, 0, limit)
 | 
						ret := make([]Article, 0, limit)
 | 
				
			||||||
	for rows.Next() {
 | 
						for rows.Next() {
 | 
				
			||||||
		a := Article{}
 | 
							a := Article{}
 | 
				
			||||||
		err := rows.Scan(&a.ID, &a.Modified, &a.Author, &a.Title)
 | 
							err := rows.Scan(&a.ArticleID, &a.Modified, &a.Author, &a.Title, &a.IsDeleted)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -213,17 +278,23 @@ func (this *WikiDB) TotalRevisions() (int64, error) {
 | 
				
			|||||||
	return ret, nil
 | 
						return ret, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiDB) ListTitles() ([]string, error) {
 | 
					func (this *WikiDB) ListTitles(includeDeleted bool) ([]TitleInfo, error) {
 | 
				
			||||||
	rows, err := this.db.Query(`SELECT title FROM titles ORDER BY title ASC`)
 | 
						stmt := `SELECT id, title, is_deleted FROM titles`
 | 
				
			||||||
 | 
						if !includeDeleted {
 | 
				
			||||||
 | 
							stmt += ` WHERE is_deleted = 0`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						stmt += ` ORDER BY title ASC`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rows, err := this.db.Query(stmt)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer rows.Close()
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ret := make([]string, 0)
 | 
						ret := make([]TitleInfo, 0)
 | 
				
			||||||
	for rows.Next() {
 | 
						for rows.Next() {
 | 
				
			||||||
		var title string
 | 
							var title TitleInfo
 | 
				
			||||||
		err = rows.Scan(&title)
 | 
							err = rows.Scan(&title.TitleID, &title.Title, &title.IsDeleted)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -236,7 +307,7 @@ func (this *WikiDB) ListTitles() ([]string, error) {
 | 
				
			|||||||
func (this *WikiDB) parseArticle(row *sql.Row) (*Article, error) {
 | 
					func (this *WikiDB) parseArticle(row *sql.Row) (*Article, error) {
 | 
				
			||||||
	a := Article{}
 | 
						a := Article{}
 | 
				
			||||||
	var gzBody []byte
 | 
						var gzBody []byte
 | 
				
			||||||
	err := row.Scan(&a.ID, &a.TitleID, &a.Modified, &gzBody, &a.Author)
 | 
						err := row.Scan(&a.ArticleID, &a.TitleID, &a.Modified, &gzBody, &a.Author)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -254,7 +325,7 @@ func (this *WikiDB) parseArticle(row *sql.Row) (*Article, error) {
 | 
				
			|||||||
func (this *WikiDB) parseArticleWithTitle(row *sql.Row) (*Article, error) {
 | 
					func (this *WikiDB) parseArticleWithTitle(row *sql.Row) (*Article, error) {
 | 
				
			||||||
	a := Article{}
 | 
						a := Article{}
 | 
				
			||||||
	var gzBody []byte
 | 
						var gzBody []byte
 | 
				
			||||||
	err := row.Scan(&a.ID, &a.TitleID, &a.Modified, &gzBody, &a.Author, &a.Title)
 | 
						err := row.Scan(&a.ArticleID, &a.TitleID, &a.Modified, &gzBody, &a.Author, &a.Title, &a.IsDeleted)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								Gopkg.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Gopkg.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[projects]]
 | 
				
			||||||
 | 
					  name = "github.com/mattn/go-sqlite3"
 | 
				
			||||||
 | 
					  packages = ["."]
 | 
				
			||||||
 | 
					  revision = "6c771bb9887719704b210e87e934f08be014bdb1"
 | 
				
			||||||
 | 
					  version = "v1.6.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[solve-meta]
 | 
				
			||||||
 | 
					  analyzer-name = "dep"
 | 
				
			||||||
 | 
					  analyzer-version = 1
 | 
				
			||||||
 | 
					  inputs-digest = "a1f2d643f8c1770c92ee1759184a0c7004af5672869db579328d05bb7cfd6bef"
 | 
				
			||||||
 | 
					  solver-name = "gps-cdcl"
 | 
				
			||||||
 | 
					  solver-version = 1
 | 
				
			||||||
							
								
								
									
										26
									
								
								Gopkg.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								Gopkg.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					# Gopkg.toml example
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
 | 
				
			||||||
 | 
					# for detailed Gopkg.toml documentation.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# required = ["github.com/user/thing/cmd/thing"]
 | 
				
			||||||
 | 
					# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# [[constraint]]
 | 
				
			||||||
 | 
					#   name = "github.com/user/project"
 | 
				
			||||||
 | 
					#   version = "1.0.0"
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# [[constraint]]
 | 
				
			||||||
 | 
					#   name = "github.com/user/project2"
 | 
				
			||||||
 | 
					#   branch = "dev"
 | 
				
			||||||
 | 
					#   source = "github.com/myfork/project2"
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# [[override]]
 | 
				
			||||||
 | 
					#  name = "github.com/x/y"
 | 
				
			||||||
 | 
					#  version = "2.4.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[constraint]]
 | 
				
			||||||
 | 
					  name = "github.com/mattn/go-sqlite3"
 | 
				
			||||||
 | 
					  version = "1.3.0"
 | 
				
			||||||
							
								
								
									
										23
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								Makefile
									
									
									
									
									
								
							@@ -2,20 +2,24 @@
 | 
				
			|||||||
# Makefile for YATWiki3
 | 
					# Makefile for YATWiki3
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
VERSION:=3.0
 | 
					VERSION:=3.3.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SOURCES:=Makefile \
 | 
					SOURCES:=Makefile \
 | 
				
			||||||
	static \
 | 
						static \
 | 
				
			||||||
	cmd $(wildcard cmd/yatwiki-server/*.go) \
 | 
						cmd $(wildcard cmd/yatwiki-server/*.go) \
 | 
				
			||||||
 | 
						Gopkg.lock Gopkg.toml \
 | 
				
			||||||
	$(wildcard *.go)
 | 
						$(wildcard *.go)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
GOFLAGS := -ldflags='-s -w' -gcflags='-trimpath=$(GOPATH)' -asmflags='-trimpath=$(GOPATH)'
 | 
					GOFLAGS:=-a \
 | 
				
			||||||
 | 
						-ldflags "-s -w -X code.ivysaur.me/yatwiki.SERVER_HEADER=YATWiki/$(VERSION)" \
 | 
				
			||||||
 | 
						-gcflags '-trimpath=$(GOPATH)' \
 | 
				
			||||||
 | 
						-asmflags '-trimpath=$(GOPATH)'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Phony targets
 | 
					# Phony targets
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
.PHONY: all dist clean
 | 
					.PHONY: all dist clean deps
 | 
				
			||||||
 | 
					
 | 
				
			||||||
all: build/linux64/yatwiki-server build/win32/yatwiki-server.exe
 | 
					all: build/linux64/yatwiki-server build/win32/yatwiki-server.exe
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -25,16 +29,21 @@ dist: \
 | 
				
			|||||||
	_dist/yatwiki-$(VERSION)-src.zip
 | 
						_dist/yatwiki-$(VERSION)-src.zip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
clean:
 | 
					clean:
 | 
				
			||||||
	if [ -f ./staticResources.go ] ; then rm ./staticResources.go ; fi
 | 
						rm -f ./staticResources.go
 | 
				
			||||||
	if [ -d ./build ] ; then rm -r ./build ; fi
 | 
						rm -fr ./build
 | 
				
			||||||
	if [ -f ./yatwiki ] ; then rm ./yatwiki ; fi
 | 
						rm -f ./yatwiki
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
					deps:
 | 
				
			||||||
 | 
						go get -u github.com/jteeuwen/go-bindata/...
 | 
				
			||||||
 | 
						go get -u github.com/golang/dep/cmd/dep
 | 
				
			||||||
 | 
						dep ensure
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Generated files
 | 
					# Generated files
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
staticResources.go: static/ static/*
 | 
					staticResources.go: static/ static/*
 | 
				
			||||||
	go-bindata -o staticResources.go -prefix static -pkg yatwiki3 static
 | 
						go-bindata -o staticResources.go -prefix static -pkg yatwiki static
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
@@ -13,6 +13,7 @@ type ServerOptions struct {
 | 
				
			|||||||
	DBFilePath           string
 | 
						DBFilePath           string
 | 
				
			||||||
	FaviconFilePath      string
 | 
						FaviconFilePath      string
 | 
				
			||||||
	AllowDBDownload      bool
 | 
						AllowDBDownload      bool
 | 
				
			||||||
 | 
						TrustXForwardedFor   bool // Introduced in 3.0.1 - default false
 | 
				
			||||||
	RecentChanges        int
 | 
						RecentChanges        int
 | 
				
			||||||
	RecentChangesRSS     int
 | 
						RecentChangesRSS     int
 | 
				
			||||||
	GzipCompressionLevel int
 | 
						GzipCompressionLevel int
 | 
				
			||||||
@@ -20,6 +21,8 @@ type ServerOptions struct {
 | 
				
			|||||||
	ExternalBaseURL      string
 | 
						ExternalBaseURL      string
 | 
				
			||||||
	DeclareRSSLanguage   string
 | 
						DeclareRSSLanguage   string
 | 
				
			||||||
	DeclareRSSEmail      string
 | 
						DeclareRSSEmail      string
 | 
				
			||||||
 | 
						ContentedServer      string
 | 
				
			||||||
 | 
						ContentedBBCodeTag   string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func DefaultOptions() *ServerOptions {
 | 
					func DefaultOptions() *ServerOptions {
 | 
				
			||||||
@@ -32,6 +35,7 @@ func DefaultOptions() *ServerOptions {
 | 
				
			|||||||
		DBFilePath:           "wiki.db",
 | 
							DBFilePath:           "wiki.db",
 | 
				
			||||||
		FaviconFilePath:      "", // no favicon
 | 
							FaviconFilePath:      "", // no favicon
 | 
				
			||||||
		AllowDBDownload:      true,
 | 
							AllowDBDownload:      true,
 | 
				
			||||||
 | 
							TrustXForwardedFor:   false,
 | 
				
			||||||
		RecentChanges:        20,
 | 
							RecentChanges:        20,
 | 
				
			||||||
		RecentChangesRSS:     10,
 | 
							RecentChangesRSS:     10,
 | 
				
			||||||
		GzipCompressionLevel: 9,
 | 
							GzipCompressionLevel: 9,
 | 
				
			||||||
@@ -39,5 +43,7 @@ func DefaultOptions() *ServerOptions {
 | 
				
			|||||||
		ExternalBaseURL:      "http://127.0.0.1/",
 | 
							ExternalBaseURL:      "http://127.0.0.1/",
 | 
				
			||||||
		DeclareRSSLanguage:   "en-GB",
 | 
							DeclareRSSLanguage:   "en-GB",
 | 
				
			||||||
		DeclareRSSEmail:      `nobody@example.com`,
 | 
							DeclareRSSEmail:      `nobody@example.com`,
 | 
				
			||||||
 | 
							ContentedServer:      "",
 | 
				
			||||||
 | 
							ContentedBBCodeTag:   "",
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"database/sql"
 | 
						"database/sql"
 | 
				
			||||||
@@ -14,6 +14,8 @@ import (
 | 
				
			|||||||
	"time"
 | 
						"time"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var SERVER_HEADER string = "YATWiki/0.0.0-devel"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type WikiServer struct {
 | 
					type WikiServer struct {
 | 
				
			||||||
	db      *WikiDB
 | 
						db      *WikiDB
 | 
				
			||||||
	opts    *ServerOptions
 | 
						opts    *ServerOptions
 | 
				
			||||||
@@ -37,7 +39,13 @@ func NewWikiServer(opts *ServerOptions) (*WikiServer, error) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	tmpl, err := template.New("yatwiki/page").Parse(pageTemplate)
 | 
						tmpl := template.New("yatwiki/page")
 | 
				
			||||||
 | 
						tmpl.Funcs(map[string]interface{}{
 | 
				
			||||||
 | 
							"pathcomponent": func(s string) string {
 | 
				
			||||||
 | 
								return url.PathEscape(s)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						_, err = tmpl.Parse(pageTemplate)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -68,12 +76,16 @@ func NewWikiServer(opts *ServerOptions) (*WikiServer, error) {
 | 
				
			|||||||
	return &ws, nil
 | 
						return &ws, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (this *WikiServer) GetBBCodeRenderer() *BBCodeRenderer {
 | 
				
			||||||
 | 
						return NewBBCodeRenderer(this.opts.ExpectBaseURL, this.opts.ContentedServer, this.opts.ContentedBBCodeTag)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiServer) Close() {
 | 
					func (this *WikiServer) Close() {
 | 
				
			||||||
	this.db.Close()
 | 
						this.db.Close()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
					func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	w.Header().Set("Server", "YATWiki3")
 | 
						w.Header().Set("Server", SERVER_HEADER)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if len(this.bans) > 0 {
 | 
						if len(this.bans) > 0 {
 | 
				
			||||||
		remoteIP := RemoteAddrToIPAddress(r.RemoteAddr)
 | 
							remoteIP := RemoteAddrToIPAddress(r.RemoteAddr)
 | 
				
			||||||
@@ -104,9 +116,13 @@ func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
			w.Write(content)
 | 
								w.Write(content)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		} else if remainingPath == "favicon.ico" && len(this.opts.FaviconFilePath) > 0 {
 | 
							} else if remainingPath == "favicon.ico" {
 | 
				
			||||||
			w.Header().Set("Content-Type", "image/x-icon")
 | 
								if len(this.opts.FaviconFilePath) > 0 {
 | 
				
			||||||
			http.ServeFile(w, r, this.opts.FaviconFilePath)
 | 
									w.Header().Set("Content-Type", "image/x-icon")
 | 
				
			||||||
 | 
									http.ServeFile(w, r, this.opts.FaviconFilePath)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									http.Error(w, "Not found", 404)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		} else if remainingPath == "download-database" {
 | 
							} else if remainingPath == "download-database" {
 | 
				
			||||||
@@ -128,22 +144,22 @@ func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		} else if remainingPath == "" {
 | 
							} else if remainingPath == "" {
 | 
				
			||||||
			this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.QueryEscape(this.opts.DefaultPage))
 | 
								this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.PathEscape(this.opts.DefaultPage))
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		} else if remainingPath == "random" {
 | 
							} else if remainingPath == "random" {
 | 
				
			||||||
			titles, err := this.db.ListTitles()
 | 
								titles, err := this.db.ListTitles(false) // "Random page" mode does not include deleted pages
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				this.serveInternalError(w, r, err)
 | 
									this.serveInternalError(w, r, err)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			chosenArticle := titles[rand.Intn(len(titles))]
 | 
								chosenArticle := titles[rand.Intn(len(titles))]
 | 
				
			||||||
			this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.QueryEscape(chosenArticle))
 | 
								this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.PathEscape(chosenArticle.Title))
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		} else if strings.HasPrefix(remainingPath, "view/") {
 | 
							} else if strings.HasPrefix(remainingPath, "view/") {
 | 
				
			||||||
			articleTitle, err := url.QueryUnescape(remainingPath[len("view/"):])
 | 
								articleTitle, err := url.PathUnescape(remainingPath[len("view/"):])
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				this.serveErrorMessage(w, err)
 | 
									this.serveErrorMessage(w, err)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
@@ -152,7 +168,7 @@ func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		} else if strings.HasPrefix(remainingPath, "modify/") {
 | 
							} else if strings.HasPrefix(remainingPath, "modify/") {
 | 
				
			||||||
			articleTitle, err := url.QueryUnescape(remainingPath[len("modify/"):])
 | 
								articleTitle, err := url.PathUnescape(remainingPath[len("modify/"):])
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				this.serveErrorMessage(w, err)
 | 
									this.serveErrorMessage(w, err)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
@@ -161,7 +177,7 @@ func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		} else if strings.HasPrefix(remainingPath, "history/") {
 | 
							} else if strings.HasPrefix(remainingPath, "history/") {
 | 
				
			||||||
			articleTitle, err := url.QueryUnescape(remainingPath[len("history/"):])
 | 
								articleTitle, err := url.PathUnescape(remainingPath[len("history/"):])
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				this.serveErrorMessage(w, err)
 | 
									this.serveErrorMessage(w, err)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
@@ -255,13 +271,13 @@ func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			err = this.db.SaveArticle(title, Author(r), body, int64(expectRev))
 | 
								err = this.db.SaveArticle(title, Author(r, this.opts.TrustXForwardedFor), body, int64(expectRev))
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				this.serveErrorMessage(w, err)
 | 
									this.serveErrorMessage(w, err)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.QueryEscape(title))
 | 
								this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.PathEscape(title))
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
A semi-anonymous wiki for use in trusted environments.
 | 
					A semi-anonymous wiki for use in trusted environments.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
As of the 20150901 release, a desktop version is available for Windows (based on PHPDesktop).
 | 
					For the 20150901 release, a desktop version is available for Windows (based on PHPDesktop).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
As of the 3.0 release, YATWiki is now a standalone server instead of a PHP script.
 | 
					As of the 3.0 release, YATWiki is now a standalone server instead of a PHP script.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -14,6 +14,7 @@ As of the 3.0 release, YATWiki is now a standalone server instead of a PHP scrip
 | 
				
			|||||||
- IP-based ban system
 | 
					- IP-based ban system
 | 
				
			||||||
- Article index, random article, download database backup
 | 
					- Article index, random article, download database backup
 | 
				
			||||||
- Source code highlighting (thanks [url=https://github.com/isagalaev/highlight.js]highlight.js[/url])
 | 
					- Source code highlighting (thanks [url=https://github.com/isagalaev/highlight.js]highlight.js[/url])
 | 
				
			||||||
 | 
					- Optional integration with `contented` for file/image uploads
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Written in Golang, PHP
 | 
					Written in Golang, PHP
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -28,9 +29,54 @@ You can start YATWiki by running the binary. A default configuration file and da
 | 
				
			|||||||
        Bind address (default "127.0.0.1:80")
 | 
					        Bind address (default "127.0.0.1:80")
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					=GO GET=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This package can be installed via go get: `go get code.ivysaur.me/yatwiki`
 | 
				
			||||||
 | 
					[go-get]code.ivysaur.me/yatwiki git https://git.ivysaur.me/code.ivysaur.me/yatwiki.git[/go-get]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
=CHANGELOG=
 | 
					=CHANGELOG=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
2017-07-11 v3.0
 | 
					2018-04-02 3.3.0
 | 
				
			||||||
 | 
					- Feature: Allow deleting (and un-deleting) articles
 | 
				
			||||||
 | 
					- Feature: Support `[youtube]` tag for embedded Youtube videos
 | 
				
			||||||
 | 
					- Feature: Add link to view raw page source when viewing a specific page revision
 | 
				
			||||||
 | 
					- Feature: Display timestamps on all console log messages
 | 
				
			||||||
 | 
					- Enhancement: Upgrade bundled SQLite3 library
 | 
				
			||||||
 | 
					- Disable zooming out on mobile devices (but zooming in is still allowed)
 | 
				
			||||||
 | 
					- Fix an issue with linking to articles where the name contained an apostrophe
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2017-11-18 3.2.0
 | 
				
			||||||
 | 
					- Feature: Add new ContentedBBCodeTag option to choose a BBCode tag for mini thumbnails (requires `contented` >= 1.2.0)
 | 
				
			||||||
 | 
					- Feature: Replace menu image with SVG, for high-DPI screens
 | 
				
			||||||
 | 
					- Feature: Hover over timestamps to display in more detail
 | 
				
			||||||
 | 
					- Feature: Link to diff pages directly from the Recent Changes page
 | 
				
			||||||
 | 
					- Fix some cosmetic issues with the Recent Changes page
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2017-10-29 3.1.3
 | 
				
			||||||
 | 
					- Fix one more case of article title normalisation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2017-10-29 3.1.2
 | 
				
			||||||
 | 
					- Lock dependency versions
 | 
				
			||||||
 | 
					- Enhancement: Advertise build number in Server headers
 | 
				
			||||||
 | 
					- Fix a regression in 3.x series with not normalising article titles
 | 
				
			||||||
 | 
					- Fix server response if favicon is not configured
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2017-10-15 3.1.1
 | 
				
			||||||
 | 
					- Update `contented` integration (requires `contented` >= 1.1.0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2017-10-08 3.1.0
 | 
				
			||||||
 | 
					- Feature: Support content upload to a `contented` server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2017-08-11 3.0.2
 | 
				
			||||||
 | 
					- Fix an issue with XSS prevention for web browsers other than Chrome
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2017-08-11 3.0.1
 | 
				
			||||||
 | 
					- Feature: New `TrustXForwardedFor` config option for usage behind reverse proxies
 | 
				
			||||||
 | 
					- Fix an issue with article titles containing `+`
 | 
				
			||||||
 | 
					- Fix an issue with `[html]` tags
 | 
				
			||||||
 | 
					- Fix an issue with viewing history for unknown articles
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2017-07-11 3.0
 | 
				
			||||||
- YATWiki was rewritten in Go.
 | 
					- YATWiki was rewritten in Go.
 | 
				
			||||||
- Enhancement: Standalone binary server
 | 
					- Enhancement: Standalone binary server
 | 
				
			||||||
- Enhancement: No longer requires cookies for error messages
 | 
					- Enhancement: No longer requires cookies for error messages
 | 
				
			||||||
@@ -43,11 +89,13 @@ You can start YATWiki by running the binary. A default configuration file and da
 | 
				
			|||||||
- Fix a number of issues with handling of base URLs in links
 | 
					- Fix a number of issues with handling of base URLs in links
 | 
				
			||||||
- Fix a cosmetic issue with file caching for CSS content
 | 
					- Fix a cosmetic issue with file caching for CSS content
 | 
				
			||||||
 | 
					
 | 
				
			||||||
2016-11-16 (no public release)
 | 
					2016-11-16 20161116
 | 
				
			||||||
 | 
					- (no public release)
 | 
				
			||||||
- Enhancement: Always open the formatting help in a new tab
 | 
					- Enhancement: Always open the formatting help in a new tab
 | 
				
			||||||
- Fix a cosmetic issue with display of backslash characters caused by Meiryo font
 | 
					- Fix a cosmetic issue with display of backslash characters caused by Meiryo font
 | 
				
			||||||
 | 
					
 | 
				
			||||||
2016-08-24 (no public release)
 | 
					2016-08-24 20160824
 | 
				
			||||||
 | 
					- (no public release)
 | 
				
			||||||
- Feature: Add Compare button to both top and bottom of article revision list
 | 
					- Feature: Add Compare button to both top and bottom of article revision list
 | 
				
			||||||
- Fix an issue with noncompliant HTML when comparing diffs
 | 
					- Fix an issue with noncompliant HTML when comparing diffs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										100
									
								
								bbcode.go
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								bbcode.go
									
									
									
									
									
								
							@@ -1,75 +1,117 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"html"
 | 
				
			||||||
	"html/template"
 | 
						"html/template"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						bbcodeIsDeleted string = `[delete]`
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// An embarassing cascade of half-working hacks follows.
 | 
					// An embarassing cascade of half-working hacks follows.
 | 
				
			||||||
type BBCodeRenderer struct {
 | 
					type BBCodeRenderer struct {
 | 
				
			||||||
	baseUrl               string
 | 
						baseUrl               string
 | 
				
			||||||
	CodePresent           bool
 | 
						CodePresent           bool
 | 
				
			||||||
	DynamicContentWarning string
 | 
						DynamicContentWarning string
 | 
				
			||||||
 | 
						ContentedURL          string
 | 
				
			||||||
 | 
						ContentedTag          string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NewBBCodeRenderer(baseUrl string) *BBCodeRenderer {
 | 
					func NewBBCodeRenderer(baseUrl, ContentedURL, ContentedTag string) *BBCodeRenderer {
 | 
				
			||||||
	return &BBCodeRenderer{
 | 
						return &BBCodeRenderer{
 | 
				
			||||||
		baseUrl:               baseUrl,
 | 
							baseUrl:               baseUrl,
 | 
				
			||||||
		CodePresent:           false,
 | 
							CodePresent:           false,
 | 
				
			||||||
		DynamicContentWarning: `⚠ run dynamic content`,
 | 
							DynamicContentWarning: `⚠ run dynamic content`,
 | 
				
			||||||
 | 
							ContentedURL:          ContentedURL,
 | 
				
			||||||
 | 
							ContentedTag:          ContentedTag,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (this *BBCodeRenderer) Reset() {
 | 
				
			||||||
 | 
						this.CodePresent = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type transformation interface {
 | 
				
			||||||
 | 
						MatchString(in string) bool
 | 
				
			||||||
 | 
						Apply(in string) string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type pregReplaceRule struct {
 | 
					type pregReplaceRule struct {
 | 
				
			||||||
	match       *regexp.Regexp
 | 
						*regexp.Regexp
 | 
				
			||||||
	replace     string
 | 
						replace string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (this pregReplaceRule) Apply(in string) string {
 | 
				
			||||||
 | 
						return this.ReplaceAllString(in, this.replace)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type pregReplaceFunc struct {
 | 
				
			||||||
 | 
						*regexp.Regexp
 | 
				
			||||||
	replaceFunc func([]string) string
 | 
						replaceFunc func([]string) string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (this pregReplaceFunc) Apply(in string) string {
 | 
				
			||||||
 | 
						return PregReplaceCallback(this.Regexp, this.replaceFunc, in)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Internal part of BBCode rendering.
 | 
					// Internal part of BBCode rendering.
 | 
				
			||||||
// It handles most leaf-level tags.
 | 
					// It handles most leaf-level tags.
 | 
				
			||||||
func (this *BBCodeRenderer) bbcode(data string) string {
 | 
					func (this *BBCodeRenderer) bbcode(data string) string {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	s_to_r := []pregReplaceRule{
 | 
						s_to_r := []transformation{
 | 
				
			||||||
		pregReplaceRule{regexp.MustCompile(`(?si)\[h\](.*?)\[/h\]`), `<h2>$1</h2>`, nil},
 | 
							pregReplaceRule{regexp.MustCompile(`(?si)\[h\](.*?)\[/h\]`), `<h2>$1</h2>`},
 | 
				
			||||||
		pregReplaceRule{regexp.MustCompile(`(?si)\[b\](.*?)\[/b\]`), `<b>$1</b>`, nil},
 | 
							pregReplaceRule{regexp.MustCompile(`(?si)\[b\](.*?)\[/b\]`), `<b>$1</b>`},
 | 
				
			||||||
		pregReplaceRule{regexp.MustCompile(`(?si)\[i\](.*?)\[/i\]`), `<i>$1</i>`, nil},
 | 
							pregReplaceRule{regexp.MustCompile(`(?si)\[i\](.*?)\[/i\]`), `<i>$1</i>`},
 | 
				
			||||||
		pregReplaceRule{regexp.MustCompile(`(?si)\[u\](.*?)\[/u\]`), `<u>$1</u>`, nil},
 | 
							pregReplaceRule{regexp.MustCompile(`(?si)\[u\](.*?)\[/u\]`), `<u>$1</u>`},
 | 
				
			||||||
		pregReplaceRule{regexp.MustCompile(`(?si)\[s\](.*?)\[/s\]`), `<span class="s">$1</span>`, nil},
 | 
							pregReplaceRule{regexp.MustCompile(`(?si)\[s\](.*?)\[/s\]`), `<span class="s">$1</span>`},
 | 
				
			||||||
		pregReplaceRule{regexp.MustCompile(`(?si)\[spoiler\](.*?)\[/spoiler\]`), `<span class="spoiler">$1</span>`, nil},
 | 
							pregReplaceRule{regexp.MustCompile(`(?si)\[spoiler\](.*?)\[/spoiler\]`), `<span class="spoiler">$1</span>`},
 | 
				
			||||||
		pregReplaceRule{regexp.MustCompile(`(?si)\[img\](.*?)\[/img\]`), `<img alt="" src="$1">`, nil},
 | 
							pregReplaceRule{regexp.MustCompile(`(?si)\[img\](.*?)\[/img\]`), `<img alt="" src="$1">`},
 | 
				
			||||||
		pregReplaceRule{regexp.MustCompile(`(?si)\[list\](.*?)\[/list\]`), `<ul><li>$1</li></ul>`, nil},
 | 
							pregReplaceRule{regexp.MustCompile(`(?si)\[list\](.*?)\[/list\]`), `<ul><li>$1</li></ul>`},
 | 
				
			||||||
		pregReplaceRule{regexp.MustCompile(`(?si)\[\*\]`), `</li><li>`, nil},
 | 
							pregReplaceRule{regexp.MustCompile(`(?si)\[\*\]`), `</li><li>`},
 | 
				
			||||||
		pregReplaceRule{regexp.MustCompile(`(?si)\[url=(.*?)\](.*?)\[/url\]`), `<a rel="noreferrer" href="$1">$2</a>`, nil},
 | 
							pregReplaceRule{regexp.MustCompile(`(?si)\[url=(.*?)\](.*?)\[/url\]`), `<a rel="noreferrer" href="$1">$2</a>`},
 | 
				
			||||||
		pregReplaceRule{regexp.MustCompile(`(?si)\[article=(.*?)\](.*?)\[/article\]`), "", func(m []string) string {
 | 
							pregReplaceFunc{regexp.MustCompile(`(?si)\[article=(.*?)\](.*?)\[/article\]`), func(m []string) string {
 | 
				
			||||||
			return `<a href="` + template.HTMLEscapeString(this.baseUrl+`view/`+url.QueryEscape(m[1])) + `">` + m[2] + `</a>`
 | 
								// m[1] has already been hesc'd
 | 
				
			||||||
 | 
								// Need to unhesc, and then pathescape
 | 
				
			||||||
 | 
								targetArticle := html.UnescapeString(m[1])
 | 
				
			||||||
 | 
								return `<a href="` + template.HTMLEscapeString(this.baseUrl+`view/`+url.PathEscape(targetArticle)) + `">` + m[2] + `</a>`
 | 
				
			||||||
		}},
 | 
							}},
 | 
				
			||||||
		pregReplaceRule{regexp.MustCompile(`(?si)\[rev=(.*?)\](.*?)\[/rev\]`), "", func(m []string) string {
 | 
							pregReplaceFunc{regexp.MustCompile(`(?si)\[rev=(.*?)\](.*?)\[/rev\]`), func(m []string) string {
 | 
				
			||||||
			return `<a href="` + template.HTMLEscapeString(this.baseUrl+`archive/`+url.QueryEscape(m[1])) + `">` + m[2] + `</a>`
 | 
								return `<a href="` + template.HTMLEscapeString(this.baseUrl+`archive/`+url.PathEscape(m[1])) + `">` + m[2] + `</a>`
 | 
				
			||||||
		}},
 | 
							}},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		pregReplaceRule{regexp.MustCompile(`(?si)\[imgur\](.*?)\.(...)\[/imgur\]`),
 | 
							pregReplaceRule{regexp.MustCompile(`(?si)\[imgur\](.*?)\.(...)\[/imgur\]`),
 | 
				
			||||||
			`<a href="https://i.imgur.com/${1}.${2}"><img class="imgur" alt="" src="https://i.imgur.com/${1}s.${2}" ></a>`,
 | 
								`<a href="https://i.imgur.com/${1}.${2}"><img class="imgur" alt="" src="https://i.imgur.com/${1}s.${2}" ></a>`,
 | 
				
			||||||
			nil,
 | 
					 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		pregReplaceRule{regexp.MustCompile(`(?si)\[section=(.*?)](.*?)\[/section\]`), "", func(m []string) string {
 | 
							pregReplaceRule{
 | 
				
			||||||
 | 
								regexp.MustCompile(`(?si)\[youtube](.*?)\[/youtube\]`),
 | 
				
			||||||
 | 
								`<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/${1}" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							pregReplaceFunc{regexp.MustCompile(`(?si)\[section=(.*?)](.*?)\[/section\]`), func(m []string) string {
 | 
				
			||||||
			return `<div class="section"><a class="sectionheader" href="javascript:;" onclick="ts(this);">` + m[1] + `</a><span style="display:none;">` + strings.TrimSpace(m[2]) + `</span></div>`
 | 
								return `<div class="section"><a class="sectionheader" href="javascript:;" onclick="ts(this);">` + m[1] + `</a><span style="display:none;">` + strings.TrimSpace(m[2]) + `</span></div>`
 | 
				
			||||||
		}},
 | 
							}},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(this.ContentedTag) > 0 {
 | 
				
			||||||
 | 
							s_to_r = append(s_to_r,
 | 
				
			||||||
 | 
								pregReplaceRule{regexp.MustCompile(`(?si)\[` + regexp.QuoteMeta(this.ContentedTag) + `\](.*?)\[/` + regexp.QuoteMeta(this.ContentedTag) + `\]`),
 | 
				
			||||||
 | 
									`<a href="` + html.EscapeString(this.ContentedURL) + `p/${1}"><img class="imgur" alt="" src="` + html.EscapeString(this.ContentedURL) + `thumb/s/${1}" ></a>`,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, prr := range s_to_r {
 | 
						for _, prr := range s_to_r {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for prr.match.MatchString(data) { // repeat until all recursive replacements are consumed
 | 
							for prr.MatchString(data) { // repeat until all recursive replacements are consumed
 | 
				
			||||||
			if len(prr.replace) > 0 {
 | 
								data = prr.Apply(data)
 | 
				
			||||||
				data = prr.match.ReplaceAllString(data, prr.replace)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				data = PregReplaceCallback(prr.match, prr.replaceFunc, data)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return data
 | 
						return data
 | 
				
			||||||
@@ -177,7 +219,7 @@ func (this *BBCodeRenderer) displayfmt(s string) string {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		epos += spos
 | 
							epos += spos
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		jsonInnerContent, _ := json.Marshal(s[spos : epos-spos])
 | 
							jsonInnerContent, _ := json.Marshal(s[spos:epos])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ret += `<div class="html"><a href="javascript:;" onclick="` + template.HTMLEscapeString(`els(this, `+string(jsonInnerContent)+`);`) + `">` + this.DynamicContentWarning + `</a></div>`
 | 
							ret += `<div class="html"><a href="javascript:;" onclick="` + template.HTMLEscapeString(`els(this, `+string(jsonInnerContent)+`);`) + `">` + this.DynamicContentWarning + `</a></div>`
 | 
				
			||||||
		hpos = epos + 7
 | 
							hpos = epos + 7
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										41
									
								
								bbcode_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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,12 +3,12 @@ package main
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"flag"
 | 
						"flag"
 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"io/ioutil"
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.ivysaur.me/yatwiki3"
 | 
						"code.ivysaur.me/yatwiki"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
@@ -17,45 +17,42 @@ func main() {
 | 
				
			|||||||
	configPath := flag.String("config", "config.json", "Configuration file")
 | 
						configPath := flag.String("config", "config.json", "Configuration file")
 | 
				
			||||||
	flag.Parse()
 | 
						flag.Parse()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	opts := yatwiki3.ServerOptions{}
 | 
						opts := yatwiki.ServerOptions{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	cfg, err := ioutil.ReadFile(*configPath)
 | 
						// Create default configuration file if necessary
 | 
				
			||||||
 | 
						if _, err := os.Stat(*configPath); os.IsNotExist(err) {
 | 
				
			||||||
	if err != nil {
 | 
							log.Printf("Creating default configuration file at '%s'...\n", *configPath)
 | 
				
			||||||
		if os.IsNotExist(err) {
 | 
							opts = *yatwiki.DefaultOptions()
 | 
				
			||||||
			opts = *yatwiki3.DefaultOptions()
 | 
							if cfg, err := json.MarshalIndent(opts, "", "\t"); err == nil {
 | 
				
			||||||
			if cfg, err := json.MarshalIndent(opts, "", "\t"); err == nil {
 | 
								err := ioutil.WriteFile(*configPath, cfg, 0644)
 | 
				
			||||||
				err := ioutil.WriteFile(*configPath, cfg, 0644)
 | 
								if err != nil {
 | 
				
			||||||
				if err != nil {
 | 
									log.Printf("Failed to save default configuration file: %s", err.Error())
 | 
				
			||||||
					fmt.Fprintf(os.Stderr, "Failed to save default configuration file: %s", err.Error())
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			fmt.Fprintf(os.Stderr, "Failed to load configuration file '%s': %s\n", *configPath, err.Error())
 | 
					 | 
				
			||||||
			os.Exit(1)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		fmt.Printf("Loading configuration from '%s'...\n", *configPath)
 | 
					 | 
				
			||||||
		err = json.Unmarshal(cfg, &opts)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			fmt.Fprintf(os.Stderr, "Failed to parse configuration file: %s\n", err.Error())
 | 
					 | 
				
			||||||
			os.Exit(1)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ws, err := yatwiki3.NewWikiServer(&opts)
 | 
						// Load configuration
 | 
				
			||||||
 | 
						log.Printf("Loading configuration from '%s'...\n", *configPath)
 | 
				
			||||||
 | 
						cfg, err := ioutil.ReadFile(*configPath)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		fmt.Fprintln(os.Stderr, err.Error())
 | 
							log.Fatalf("Failed to load configuration file '%s': %s\n", *configPath, err.Error())
 | 
				
			||||||
		os.Exit(1)
 | 
						}
 | 
				
			||||||
 | 
						err = json.Unmarshal(cfg, &opts)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("Failed to parse configuration file: %s\n", err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ws, err := yatwiki.NewWikiServer(&opts)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatalln(err.Error())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer ws.Close()
 | 
						defer ws.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmt.Printf("YATWiki now listening on %s\n", *bindAddr)
 | 
						log.Printf("YATWiki now listening on %s\n", *bindAddr)
 | 
				
			||||||
	err = http.ListenAndServe(*bindAddr, ws)
 | 
						err = http.ListenAndServe(*bindAddr, ws)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		fmt.Fprintln(os.Stderr, err.Error())
 | 
							log.Fatalln(err.Error())
 | 
				
			||||||
		os.Exit(1)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	os.Exit(0)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@ import (
 | 
				
			|||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.ivysaur.me/yatwiki3/diff"
 | 
						"code.ivysaur.me/yatwiki/diff"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestDiff(t *testing.T) {
 | 
					func TestDiff(t *testing.T) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
@@ -11,13 +11,17 @@ var subresourceNonce = time.Now().Unix()
 | 
				
			|||||||
type pageTemplateOptions struct {
 | 
					type pageTemplateOptions struct {
 | 
				
			||||||
	CurrentPageIsArticle bool
 | 
						CurrentPageIsArticle bool
 | 
				
			||||||
	CurrentPageName      string
 | 
						CurrentPageName      string
 | 
				
			||||||
 | 
						CurrentPageIsRev     bool
 | 
				
			||||||
 | 
						CurrentPageRev       int64
 | 
				
			||||||
	WikiTitle            string
 | 
						WikiTitle            string
 | 
				
			||||||
	Content              template.HTML
 | 
						Content              template.HTML
 | 
				
			||||||
	BaseURL              string
 | 
						BaseURL              string
 | 
				
			||||||
	LoadCodeResources    bool
 | 
						LoadCodeResources    bool
 | 
				
			||||||
	DefaultPage          string
 | 
						DefaultPage          string
 | 
				
			||||||
	AllowDownload        bool
 | 
						AllowDownload        bool
 | 
				
			||||||
	SessionMessage       template.HTML
 | 
						SessionMessage       string
 | 
				
			||||||
 | 
						PageNotExistsError   bool
 | 
				
			||||||
 | 
						PageNotExistsTarget  string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func DefaultPageTemplateOptions(opts *ServerOptions) *pageTemplateOptions {
 | 
					func DefaultPageTemplateOptions(opts *ServerOptions) *pageTemplateOptions {
 | 
				
			||||||
@@ -42,7 +46,7 @@ const pageTemplate string = `<!DOCTYPE html>
 | 
				
			|||||||
	<head>
 | 
						<head>
 | 
				
			||||||
		<title>{{.CurrentPageName}}{{ if len .CurrentPageName }} - {{end}}{{.WikiTitle}}</title>		
 | 
							<title>{{.CurrentPageName}}{{ if len .CurrentPageName }} - {{end}}{{.WikiTitle}}</title>		
 | 
				
			||||||
		<meta charset="utf-8">
 | 
							<meta charset="utf-8">
 | 
				
			||||||
		<meta name="viewport" content="width=device-width">
 | 
							<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
 | 
				
			||||||
		<meta name="referrer" content="no-referrer">
 | 
							<meta name="referrer" content="no-referrer">
 | 
				
			||||||
		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
 | 
							<meta http-equiv="X-UA-Compatible" content="IE=edge" />
 | 
				
			||||||
		<link rel="alternate" type="application/rss+xml" href="{{.BaseURL}}rss/changes" title="{{.WikiTitle}} - Recent Changes">
 | 
							<link rel="alternate" type="application/rss+xml" href="{{.BaseURL}}rss/changes" title="{{.WikiTitle}} - Recent Changes">
 | 
				
			||||||
@@ -85,26 +89,63 @@ function els(e,s){ // no js exec in innerHTML
 | 
				
			|||||||
	</head>
 | 
						</head>
 | 
				
			||||||
	<body>
 | 
						<body>
 | 
				
			||||||
		<div class="header">
 | 
							<div class="header">
 | 
				
			||||||
			<a href="{{.BaseURL}}view/{{.DefaultPage | urlquery}}" title="Home"><div class="sprite hm"></div></a>
 | 
								<a href="{{.BaseURL}}view/{{.DefaultPage | pathcomponent}}" title="Home"><div class="sprite">
 | 
				
			||||||
			<a href="javascript:;" onclick="tid('spm');tid('tr1');tid('tr2');" title="Menu"><div class="sprite sp"></div></a>
 | 
									<svg viewBox="0 0 24 24">
 | 
				
			||||||
			<a href="{{.BaseURL}}modify/{{.NewArticleTitle | urlquery}}" title="New Page"><div class="sprite nw"></div></a>
 | 
					    				<path d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" />
 | 
				
			||||||
 | 
									</svg>
 | 
				
			||||||
 | 
								</div></a>
 | 
				
			||||||
 | 
								<a href="javascript:;" onclick="tid('spm');tid('tr1');tid('tr2');" title="Menu"><div class="sprite">
 | 
				
			||||||
 | 
									<svg viewBox="0 0 24 24">
 | 
				
			||||||
 | 
					    				<path d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z" />
 | 
				
			||||||
 | 
									</svg>
 | 
				
			||||||
 | 
								</div></a>
 | 
				
			||||||
 | 
								<a href="{{.BaseURL}}modify/{{.NewArticleTitle | pathcomponent}}" title="New Page"><div class="sprite">
 | 
				
			||||||
 | 
									<svg viewBox="0 0 24 24">
 | 
				
			||||||
 | 
									    <path d="M13,9V3.5L18.5,9M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6Z" />
 | 
				
			||||||
 | 
									</svg>
 | 
				
			||||||
 | 
								</div></a>
 | 
				
			||||||
{{if .CurrentPageIsArticle }}
 | 
					{{if .CurrentPageIsArticle }}
 | 
				
			||||||
			<div class="sep"></div>
 | 
								<div class="sep"></div>
 | 
				
			||||||
			<a href="{{.BaseURL}}history/{{.CurrentPageName | urlquery}}" title="Page History"><div class="sprite hs"></div></a>
 | 
								<a href="{{.BaseURL}}history/{{.CurrentPageName | pathcomponent}}" title="Page History"><div class="sprite">
 | 
				
			||||||
			<a href="{{.BaseURL}}modify/{{.CurrentPageName | urlquery}}" title="Modify Page"><div class="sprite ed"></div></a>
 | 
									<svg viewBox="0 0 24 24">
 | 
				
			||||||
 | 
									    <path d="M15,13H16.5V15.82L18.94,17.23L18.19,18.53L15,16.69V13M19,8H5V19H9.67C9.24,18.09 9,17.07 9,16A7,7 0 0,1 16,9C17.07,9 18.09,9.24 19,9.67V8M5,21C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H6V1H8V3H16V1H18V3H19A2,2 0 0,1 21,5V11.1C22.24,12.36 23,14.09 23,16A7,7 0 0,1 16,23C14.09,23 12.36,22.24 11.1,21H5M16,11.15A4.85,4.85 0 0,0 11.15,16C11.15,18.68 13.32,20.85 16,20.85A4.85,4.85 0 0,0 20.85,16C20.85,13.32 18.68,11.15 16,11.15Z" />
 | 
				
			||||||
 | 
									</svg>
 | 
				
			||||||
 | 
								</div></a>
 | 
				
			||||||
 | 
								<a href="{{.BaseURL}}modify/{{.CurrentPageName | pathcomponent}}" title="Modify Page"><div class="sprite">
 | 
				
			||||||
 | 
									<svg viewBox="0 0 24 24">
 | 
				
			||||||
 | 
									    <path d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z" />
 | 
				
			||||||
 | 
									</svg>
 | 
				
			||||||
 | 
								</div></a>
 | 
				
			||||||
 | 
						{{if .CurrentPageIsRev }}
 | 
				
			||||||
 | 
								<a href="{{.BaseURL}}raw/{{.CurrentPageRev}}" title="Page Source"><div class="sprite">
 | 
				
			||||||
 | 
									<svg viewBox="0 0 24 24">
 | 
				
			||||||
 | 
									    <path d="M8,3A2,2 0 0,0 6,5V9A2,2 0 0,1 4,11H3V13H4A2,2 0 0,1 6,15V19A2,2 0 0,0 8,21H10V19H8V14A2,2 0 0,0 6,12A2,2 0 0,0 8,10V5H10V3M16,3A2,2 0 0,1 18,5V9A2,2 0 0,0 20,11H21V13H20A2,2 0 0,0 18,15V19A2,2 0 0,1 16,21H14V19H16V14A2,2 0 0,1 18,12A2,2 0 0,1 16,10V5H14V3H16Z" />
 | 
				
			||||||
 | 
									</svg>
 | 
				
			||||||
 | 
								</div></a>
 | 
				
			||||||
 | 
						{{end}}
 | 
				
			||||||
{{end}}
 | 
					{{end}}
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<div id="tr1" style="display:none;"></div>
 | 
							<div id="tr1" style="display:none;"></div>
 | 
				
			||||||
		<div id="tr2" style="display:none;"></div>
 | 
							<div id="tr2" style="display:none;"></div>
 | 
				
			||||||
		<div class="ddmenu" id="spm" style="display:none;">
 | 
							<div class="ddmenu" id="spm" style="display:none;">
 | 
				
			||||||
			<a href="{{.BaseURL}}recent/1"><div class="sprite no"></div> Recent Changes</a>
 | 
								<a href="{{.BaseURL}}recent/1"><div class="sprite"></div> Recent Changes</a>
 | 
				
			||||||
			<a href="{{.BaseURL}}random"><div class="sprite rn"></div> Random Page</a>
 | 
								<a href="{{.BaseURL}}random"><div class="sprite">
 | 
				
			||||||
			<a href="{{.BaseURL}}index"><div class="sprite no"></div> Article Index</a>
 | 
									<svg viewBox="0 0 24 24">
 | 
				
			||||||
 | 
									    <path d="M12,18A6,6 0 0,1 6,12C6,11 6.25,10.03 6.7,9.2L5.24,7.74C4.46,8.97 4,10.43 4,12A8,8 0 0,0 12,20V23L16,19L12,15M12,4V1L8,5L12,9V6A6,6 0 0,1 18,12C18,13 17.75,13.97 17.3,14.8L18.76,16.26C19.54,15.03 20,13.57 20,12A8,8 0 0,0 12,4Z" />
 | 
				
			||||||
 | 
									</svg>
 | 
				
			||||||
 | 
								</div> Random Page</a>
 | 
				
			||||||
 | 
								<a href="{{.BaseURL}}index"><div class="sprite"></div> Article Index</a>
 | 
				
			||||||
{{if .AllowDownload}}
 | 
					{{if .AllowDownload}}
 | 
				
			||||||
			<a href="{{.BaseURL}}download-database" download><div class="sprite no"></div> Download DB backup</a>
 | 
								<a href="{{.BaseURL}}download-database" download><div class="sprite"></div> Download DB backup</a>
 | 
				
			||||||
{{end}}
 | 
					{{end}}
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<div class="content">
 | 
							<div class="content">
 | 
				
			||||||
 | 
					{{if .PageNotExistsError}}
 | 
				
			||||||
 | 
								<div class="info">
 | 
				
			||||||
 | 
									No such article exists.
 | 
				
			||||||
 | 
									<a href="{{.BaseURL}}modify/{{.PageNotExistsTarget | pathcomponent}}">Click here</a> to create it.
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
{{if len .SessionMessage}}
 | 
					{{if len .SessionMessage}}
 | 
				
			||||||
			<div class="info">{{.SessionMessage}}</div>
 | 
								<div class="info">{{.SessionMessage}}</div>
 | 
				
			||||||
{{end}}
 | 
					{{end}}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										19
									
								
								rArchive.go
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								rArchive.go
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"database/sql"
 | 
						"database/sql"
 | 
				
			||||||
@@ -25,13 +25,18 @@ func (this *WikiServer) routeArchive(w http.ResponseWriter, r *http.Request, rev
 | 
				
			|||||||
	pto.CurrentPageName = a.Title
 | 
						pto.CurrentPageName = a.Title
 | 
				
			||||||
	pto.CurrentPageIsArticle = true
 | 
						pto.CurrentPageIsArticle = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	bcr := NewBBCodeRenderer(this.opts.ExpectBaseURL)
 | 
						pto.CurrentPageRev = int64(revId)
 | 
				
			||||||
 | 
						pto.CurrentPageIsRev = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						infoMessageHtml := `You are viewing a specific revision of this page, last modified ` +
 | 
				
			||||||
 | 
							time.Unix(a.Modified, 0).In(this.loc).Format(this.opts.DateFormat) + `. `
 | 
				
			||||||
 | 
						if !a.IsDeleted {
 | 
				
			||||||
 | 
							infoMessageHtml += `Click <a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(a.Title)) + `">here</a> to see the latest revision.`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						bcr := this.GetBBCodeRenderer()
 | 
				
			||||||
	pto.Content = template.HTML(
 | 
						pto.Content = template.HTML(
 | 
				
			||||||
		`<div class="info">`+
 | 
							`<div class="info">`+infoMessageHtml+`</div>`,
 | 
				
			||||||
			`You are viewing specific revision of this page, last modified `+
 | 
					 | 
				
			||||||
			time.Unix(a.Modified, 0).In(this.loc).Format(this.opts.DateFormat)+`. `+
 | 
					 | 
				
			||||||
			`Click <a href="`+template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.QueryEscape(a.Title))+`">here</a> to see the latest revision.`+
 | 
					 | 
				
			||||||
			`</div>`,
 | 
					 | 
				
			||||||
	) + bcr.RenderHTML(string(a.Body))
 | 
						) + bcr.RenderHTML(string(a.Body))
 | 
				
			||||||
	pto.LoadCodeResources = bcr.CodePresent
 | 
						pto.LoadCodeResources = bcr.CodePresent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								rDiff.go
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								rDiff.go
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
@@ -8,7 +8,7 @@ import (
 | 
				
			|||||||
	"html/template"
 | 
						"html/template"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.ivysaur.me/yatwiki3/diff"
 | 
						"code.ivysaur.me/yatwiki/diff"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiServer) routeDiff(w http.ResponseWriter, r *http.Request, oldRev, newRev int) {
 | 
					func (this *WikiServer) routeDiff(w http.ResponseWriter, r *http.Request, oldRev, newRev int) {
 | 
				
			||||||
@@ -53,9 +53,9 @@ func (this *WikiServer) routeDiff(w http.ResponseWriter, r *http.Request, oldRev
 | 
				
			|||||||
	pto.Content = template.HTML(
 | 
						pto.Content = template.HTML(
 | 
				
			||||||
		`<h2>` +
 | 
							`<h2>` +
 | 
				
			||||||
			`Comparing ` + string(this.viewLink(oa.Title)) + ` versions ` +
 | 
								`Comparing ` + string(this.viewLink(oa.Title)) + ` versions ` +
 | 
				
			||||||
			`<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", oa.ID)) + `">r` + fmt.Sprintf("%d", oa.ID) + `</a>` +
 | 
								`<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", oa.ArticleID)) + `">r` + fmt.Sprintf("%d", oa.ArticleID) + `</a>` +
 | 
				
			||||||
			` - ` +
 | 
								` - ` +
 | 
				
			||||||
			`<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", na.ID)) + `">r` + fmt.Sprintf("%d", na.ID) + `</a>` +
 | 
								`<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", na.ArticleID)) + `">r` + fmt.Sprintf("%d", na.ArticleID) + `</a>` +
 | 
				
			||||||
			`</h2>` +
 | 
								`</h2>` +
 | 
				
			||||||
			`<pre>` + string(b.Bytes()) + `</pre>`,
 | 
								`<pre>` + string(b.Bytes()) + `</pre>`,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										39
									
								
								rErrors.go
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								rErrors.go
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"html/template"
 | 
						"html/template"
 | 
				
			||||||
@@ -8,12 +8,8 @@ import (
 | 
				
			|||||||
	"time"
 | 
						"time"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiServer) noSuchArticleError(title string) template.HTML {
 | 
					func (this *WikiServer) serveErrorMessage(w http.ResponseWriter, err error) {
 | 
				
			||||||
	return template.HTML(`No such article exists. <a href="` + this.opts.ExpectBaseURL + `modify/` + template.HTMLEscapeString(url.QueryEscape(title)) + `">Click here</a> to create it.`)
 | 
						this.serveErrorText(w, err.Error())
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (this *WikiServer) serveErrorMessage(w http.ResponseWriter, message error) {
 | 
					 | 
				
			||||||
	this.serveErrorHTMLMessage(w, template.HTML(template.HTMLEscapeString(message.Error())))
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiServer) serveInternalError(w http.ResponseWriter, r *http.Request, e error) {
 | 
					func (this *WikiServer) serveInternalError(w http.ResponseWriter, r *http.Request, e error) {
 | 
				
			||||||
@@ -21,8 +17,16 @@ func (this *WikiServer) serveInternalError(w http.ResponseWriter, r *http.Reques
 | 
				
			|||||||
	http.Error(w, "An internal error occurred. Please ask an administrator to check the log file.", 500)
 | 
						http.Error(w, "An internal error occurred. Please ask an administrator to check the log file.", 500)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiServer) serveErrorHTMLMessage(w http.ResponseWriter, msg template.HTML) {
 | 
					func (this *WikiServer) serveDeleted(w http.ResponseWriter, lookingFor string) {
 | 
				
			||||||
	this.serveRedirect(w, this.opts.ExpectBaseURL+"view/"+url.QueryEscape(this.opts.DefaultPage)+"?error="+url.QueryEscape(string(msg)))
 | 
						this.serveRedirect(w, this.opts.ExternalBaseURL+"history/"+url.PathEscape(lookingFor)+"?error="+url.QueryEscape(`The page you are looking for has been deleted. However, the history is still available.`))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (this *WikiServer) serveErrorText(w http.ResponseWriter, msg string) {
 | 
				
			||||||
 | 
						this.serveRedirect(w, this.opts.ExpectBaseURL+"view/"+url.PathEscape(this.opts.DefaultPage)+"?error="+url.QueryEscape(msg))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (this *WikiServer) serveNoSuchArticle(w http.ResponseWriter, lookingFor string) {
 | 
				
			||||||
 | 
						this.serveRedirect(w, this.opts.ExpectBaseURL+"view/"+url.PathEscape(this.opts.DefaultPage)+"?notfound="+url.QueryEscape(lookingFor))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiServer) serveRedirect(w http.ResponseWriter, location string) {
 | 
					func (this *WikiServer) serveRedirect(w http.ResponseWriter, location string) {
 | 
				
			||||||
@@ -32,7 +36,14 @@ func (this *WikiServer) serveRedirect(w http.ResponseWriter, location string) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (this *WikiServer) servePageResponse(w http.ResponseWriter, r *http.Request, pto *pageTemplateOptions) {
 | 
					func (this *WikiServer) servePageResponse(w http.ResponseWriter, r *http.Request, pto *pageTemplateOptions) {
 | 
				
			||||||
	w.WriteHeader(200)
 | 
						w.WriteHeader(200)
 | 
				
			||||||
	pto.SessionMessage = template.HTML(r.URL.Query().Get("error")) // FIXME reflected XSS (although Chrome automatically blocks it..)
 | 
					
 | 
				
			||||||
 | 
						if noSuchArticleTarget, ok := r.URL.Query()["notfound"]; ok {
 | 
				
			||||||
 | 
							pto.PageNotExistsError = true
 | 
				
			||||||
 | 
							pto.PageNotExistsTarget = noSuchArticleTarget[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							pto.SessionMessage = r.URL.Query().Get("error")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err := this.pageTmp.Execute(w, pto)
 | 
						err := this.pageTmp.Execute(w, pto)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -40,11 +51,13 @@ func (this *WikiServer) servePageResponse(w http.ResponseWriter, r *http.Request
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiServer) formatTimestamp(m int64) string {
 | 
					func (this *WikiServer) formatTimestamp(m int64) template.HTML {
 | 
				
			||||||
	// TODO add a more detailed timestamp on hover
 | 
						// TODO add a more detailed timestamp on hover
 | 
				
			||||||
	return template.HTMLEscapeString(time.Unix(m, 0).In(this.loc).Format(this.opts.DateFormat))
 | 
						dt := time.Unix(m, 0).In(this.loc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return template.HTML(`<span title="` + template.HTMLEscapeString(dt.Format(time.RFC3339)) + `">` + template.HTMLEscapeString(dt.Format(this.opts.DateFormat)) + `</span>`)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiServer) viewLink(articleTitle string) template.HTML {
 | 
					func (this *WikiServer) viewLink(articleTitle string) template.HTML {
 | 
				
			||||||
	return template.HTML(`"<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.QueryEscape(articleTitle)) + `">` + template.HTMLEscapeString(articleTitle) + `</a>"`)
 | 
						return template.HTML(`"<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(articleTitle)) + `">` + template.HTMLEscapeString(articleTitle) + `</a>"`)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,15 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"html/template"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiServer) routeFormatting(w http.ResponseWriter, r *http.Request) {
 | 
					func (this *WikiServer) routeFormatting(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	pto := DefaultPageTemplateOptions(this.opts)
 | 
						pto := DefaultPageTemplateOptions(this.opts)
 | 
				
			||||||
	pto.CurrentPageName = "Formatting help"
 | 
						pto.CurrentPageName = "Formatting help"
 | 
				
			||||||
	pto.Content = `
 | 
					
 | 
				
			||||||
 | 
						content := `
 | 
				
			||||||
<h2>Formatting help</h2><br><br>
 | 
					<h2>Formatting help</h2><br><br>
 | 
				
			||||||
<ul>
 | 
					<ul>
 | 
				
			||||||
	<li>[h]header[/h]</li>
 | 
						<li>[h]header[/h]</li>
 | 
				
			||||||
@@ -21,9 +23,22 @@ func (this *WikiServer) routeFormatting(w http.ResponseWriter, r *http.Request)
 | 
				
			|||||||
	<li>[article=page name]title[/article] or [rev=id]title[/rev]</li>
 | 
						<li>[article=page name]title[/article] or [rev=id]title[/rev]</li>
 | 
				
			||||||
	<li>[img]image-url[/img]</li>
 | 
						<li>[img]image-url[/img]</li>
 | 
				
			||||||
	<li>[imgur]asdf.jpg[/imgur]</li>
 | 
						<li>[imgur]asdf.jpg[/imgur]</li>
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
						if len(this.opts.ContentedBBCodeTag) > 0 {
 | 
				
			||||||
 | 
							content += `
 | 
				
			||||||
 | 
						<li>[` + this.opts.ContentedBBCodeTag + `]abc[/` + this.opts.ContentedBBCodeTag + `]</li>
 | 
				
			||||||
 | 
							`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						content += `
 | 
				
			||||||
	<li>[code]fixed width[/code]</li>
 | 
						<li>[code]fixed width[/code]</li>
 | 
				
			||||||
	<li>[section=header]content[/section]</li>
 | 
						<li>[section=header]content[/section]</li>
 | 
				
			||||||
 | 
						<li>[youtube]id[/youtube]</li>
 | 
				
			||||||
	<li>[html]raw html[/html]</li>
 | 
						<li>[html]raw html[/html]</li>
 | 
				
			||||||
 | 
						<li>` + bbcodeIsDeleted + `</li>
 | 
				
			||||||
</ul>`
 | 
					</ul>`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pto.Content = template.HTML(content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	this.servePageResponse(w, r, pto)
 | 
						this.servePageResponse(w, r, pto)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								rHistory.go
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								rHistory.go
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"database/sql"
 | 
						"database/sql"
 | 
				
			||||||
@@ -12,7 +12,7 @@ func (this *WikiServer) routeHistory(w http.ResponseWriter, r *http.Request, art
 | 
				
			|||||||
	revs, err := this.db.GetRevisionHistory(articleTitle)
 | 
						revs, err := this.db.GetRevisionHistory(articleTitle)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if err == sql.ErrNoRows {
 | 
							if err == sql.ErrNoRows {
 | 
				
			||||||
			this.serveErrorHTMLMessage(w, this.noSuchArticleError(articleTitle))
 | 
								this.serveNoSuchArticle(w, articleTitle)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -20,21 +20,26 @@ func (this *WikiServer) routeHistory(w http.ResponseWriter, r *http.Request, art
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(revs) == 0 {
 | 
				
			||||||
 | 
							this.serveNoSuchArticle(w, articleTitle)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	pto := DefaultPageTemplateOptions(this.opts)
 | 
						pto := DefaultPageTemplateOptions(this.opts)
 | 
				
			||||||
	pto.CurrentPageName = articleTitle
 | 
						pto.CurrentPageName = articleTitle
 | 
				
			||||||
	pto.CurrentPageIsArticle = true
 | 
						pto.CurrentPageIsArticle = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	content := `<h2>Page History</h2><br>` +
 | 
						content := `<h2>Page History</h2><br>` +
 | 
				
			||||||
		`<em>There have been ` + fmt.Sprintf("%d", len(revs)) + ` edits to the page "<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.QueryEscape(articleTitle)) + `">` + template.HTMLEscapeString(articleTitle) + `</a>".</em>` +
 | 
							`<em>There have been ` + fmt.Sprintf("%d", len(revs)) + ` edits to the page "<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(articleTitle)) + `">` + template.HTMLEscapeString(articleTitle) + `</a>".</em>` +
 | 
				
			||||||
		`<br><br>` +
 | 
							`<br><br>` +
 | 
				
			||||||
		`<form method="GET" action="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`diff`) + `">` +
 | 
							`<form method="GET" action="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`diff`) + `">` +
 | 
				
			||||||
		`<table>`
 | 
							`<table>`
 | 
				
			||||||
	compareRow := `<tr><td colspan="2"></td><td><input type="submit" value="Compare Selected »"></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) + `">` + this.formatTimestamp(rev.Modified) + `</a></td>` +
 | 
								`<td><a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+revIdStr) + `">` + string(this.formatTimestamp(rev.Modified)) + `</a></td>` +
 | 
				
			||||||
			`<td>` + template.HTMLEscapeString(rev.Author) + `</td>` +
 | 
								`<td>` + template.HTMLEscapeString(rev.Author) + `</td>` +
 | 
				
			||||||
			`<td><input type="radio" name="t" value="` + revIdStr + `"> <input type="radio" name="f" value="` + revIdStr + `"></td>` +
 | 
								`<td><input type="radio" name="t" value="` + revIdStr + `"> <input type="radio" name="f" value="` + revIdStr + `"></td>` +
 | 
				
			||||||
			`</tr>`
 | 
								`</tr>`
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										26
									
								
								rIndex.go
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								rIndex.go
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
@@ -8,12 +8,15 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *WikiServer) routeIndex(w http.ResponseWriter, r *http.Request) {
 | 
					func (this *WikiServer) routeIndex(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	titles, err := this.db.ListTitles()
 | 
						titles, err := this.db.ListTitles(true) // Always load deleted pages, even if we don't display them in the list
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		this.serveInternalError(w, r, err)
 | 
							this.serveInternalError(w, r, err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						showDeleted := (r.FormValue("deleted") == "1")
 | 
				
			||||||
 | 
						anyDeleted := false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	totalRevs, err := this.db.TotalRevisions()
 | 
						totalRevs, err := this.db.TotalRevisions()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		this.serveInternalError(w, r, err)
 | 
							this.serveInternalError(w, r, err)
 | 
				
			||||||
@@ -22,10 +25,27 @@ func (this *WikiServer) routeIndex(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	content := fmt.Sprintf(`<h2>Article Index</h2><br><em>There are %d edits to %d pages.</em><br><br><ul>`, totalRevs, len(titles))
 | 
						content := fmt.Sprintf(`<h2>Article Index</h2><br><em>There are %d edits to %d pages.</em><br><br><ul>`, totalRevs, len(titles))
 | 
				
			||||||
	for _, title := range titles {
 | 
						for _, title := range titles {
 | 
				
			||||||
		content += `<li><a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.QueryEscape(title)) + `">` + template.HTMLEscapeString(title) + `</a></li>`
 | 
							classAttr := ""
 | 
				
			||||||
 | 
							if title.IsDeleted {
 | 
				
			||||||
 | 
								anyDeleted = true
 | 
				
			||||||
 | 
								if !showDeleted {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								classAttr = `class="deleted"`
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							content += `<li><a ` + classAttr + ` href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(title.Title)) + `">` + template.HTMLEscapeString(title.Title) + `</a></li>`
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	content += `</ul>`
 | 
						content += `</ul>`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if anyDeleted {
 | 
				
			||||||
 | 
							content += `<br>`
 | 
				
			||||||
 | 
							if !showDeleted {
 | 
				
			||||||
 | 
								content += `<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`index?deleted=1`) + `">Show deleted pages</a>`
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								content += `<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`index`) + `">Hide deleted pages</a>`
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	pto := DefaultPageTemplateOptions(this.opts)
 | 
						pto := DefaultPageTemplateOptions(this.opts)
 | 
				
			||||||
	pto.CurrentPageName = "Index"
 | 
						pto.CurrentPageName = "Index"
 | 
				
			||||||
	pto.Content = template.HTML(content)
 | 
						pto.Content = template.HTML(content)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										34
									
								
								rModify.go
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								rModify.go
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"database/sql"
 | 
						"database/sql"
 | 
				
			||||||
@@ -32,8 +32,8 @@ func (this *WikiServer) routeModify(w http.ResponseWriter, r *http.Request, arti
 | 
				
			|||||||
		pageTitleHTML = `Creating new article`
 | 
							pageTitleHTML = `Creating new article`
 | 
				
			||||||
		baseRev = 0
 | 
							baseRev = 0
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		pageTitleHTML = `Editing article "<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.QueryEscape(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)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -49,6 +49,34 @@ func (this *WikiServer) routeModify(w http.ResponseWriter, r *http.Request, arti
 | 
				
			|||||||
		</label>
 | 
							</label>
 | 
				
			||||||
		<input type="submit" value="Save »">
 | 
							<input type="submit" value="Save »">
 | 
				
			||||||
		| <a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`formatting`) + `" target="_blank">formatting help</a>
 | 
							| <a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`formatting`) + `" target="_blank">formatting help</a>
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
						if len(this.opts.ContentedServer) > 0 {
 | 
				
			||||||
 | 
							content += `
 | 
				
			||||||
 | 
					<script type="text/javascript" src="` + this.opts.ContentedServer + `sdk.js"></script>
 | 
				
			||||||
 | 
							| <a href="javascript:;" id="open-contented-uploader">upload...</a>
 | 
				
			||||||
 | 
							<script type="text/javascript">
 | 
				
			||||||
 | 
								document.getElementById("open-contented-uploader").addEventListener("click", function() {
 | 
				
			||||||
 | 
									contented.init("#contentctr", function(items) {
 | 
				
			||||||
 | 
										for (var i = 0; i < items.length; ++i) {
 | 
				
			||||||
 | 
											$("#contentctr textarea").append(" " + contented.getPreviewURL(items[i]) + " ");
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							</script>
 | 
				
			||||||
 | 
							`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						content += `
 | 
				
			||||||
 | 
							|
 | 
				
			||||||
 | 
							<a href="javascript:;" id="delete-page">delete</a>
 | 
				
			||||||
 | 
							<script type="text/javascript">
 | 
				
			||||||
 | 
								document.getElementById("delete-page").addEventListener("click", function() {
 | 
				
			||||||
 | 
									if (confirm('Are you sure you want to delete this page?\nThe history will be preserved.')) {
 | 
				
			||||||
 | 
										document.getElementsByName("content")[0].value = '[delete]';
 | 
				
			||||||
 | 
										document.getElementById("form-edit-page").submit();
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							</script>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<div id="contentctr"><textarea name="content">` + template.HTMLEscapeString(existingBody) + `</textarea></div>
 | 
						<div id="contentctr"><textarea name="content">` + template.HTMLEscapeString(existingBody) + `</textarea></div>
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										14
									
								
								rRSS.go
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								rRSS.go
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
@@ -27,17 +27,17 @@ func (this *WikiServer) routeRecentChangesRSS(w http.ResponseWriter, r *http.Req
 | 
				
			|||||||
	for _, a := range recents {
 | 
						for _, a := range recents {
 | 
				
			||||||
		content += `
 | 
							content += `
 | 
				
			||||||
		<item>
 | 
							<item>
 | 
				
			||||||
			<title>` + template.HTMLEscapeString(a.Title+` (r`+fmt.Sprintf("%d", a.ID)+`)`) + `</title>
 | 
								<title>` + template.HTMLEscapeString(a.Title+` (r`+fmt.Sprintf("%d", a.ArticleID)+`)`) + `</title>
 | 
				
			||||||
			<link>` + template.HTMLEscapeString(this.opts.ExternalBaseURL+`archive/`+fmt.Sprintf("%d", a.ID)) + `</link>
 | 
								<link>` + template.HTMLEscapeString(this.opts.ExternalBaseURL+`archive/`+fmt.Sprintf("%d", a.ArticleID)) + `</link>
 | 
				
			||||||
			<guid>` + template.HTMLEscapeString(this.opts.ExternalBaseURL+`archive/`+fmt.Sprintf("%d", a.ID)) + `</guid>
 | 
								<guid>` + template.HTMLEscapeString(this.opts.ExternalBaseURL+`archive/`+fmt.Sprintf("%d", a.ArticleID)) + `</guid>
 | 
				
			||||||
			<author>` + template.HTMLEscapeString(this.opts.DeclareRSSEmail+` (`+this.opts.PageTitle+` `+a.Author+`)`) + `</author>
 | 
								<author>` + template.HTMLEscapeString(this.opts.DeclareRSSEmail+` (`+this.opts.PageTitle+` `+a.Author+`)`) + `</author>
 | 
				
			||||||
			<pubDate>` + template.HTMLEscapeString(time.Unix(a.Modified, 0).In(this.loc).Format(time.RFC1123Z)) + `</pubDate>
 | 
								<pubDate>` + template.HTMLEscapeString(time.Unix(a.Modified, 0).In(this.loc).Format(time.RFC1123Z)) + `</pubDate>
 | 
				
			||||||
			<description>` + template.HTMLEscapeString(`
 | 
								<description>` + template.HTMLEscapeString(`
 | 
				
			||||||
				<a href="`+template.HTMLEscapeString(this.opts.ExternalBaseURL+`view/`+url.QueryEscape(a.Title))+`">latest version</a>
 | 
									<a href="`+template.HTMLEscapeString(this.opts.ExternalBaseURL+`view/`+url.PathEscape(a.Title))+`">latest version</a>
 | 
				
			||||||
				|
 | 
									|
 | 
				
			||||||
				<a href="`+template.HTMLEscapeString(this.opts.ExternalBaseURL+`archive/`+fmt.Sprintf("%d", a.ID))+`">revision `+fmt.Sprintf("%d", a.ID)+`</a>
 | 
									<a href="`+template.HTMLEscapeString(this.opts.ExternalBaseURL+`archive/`+fmt.Sprintf("%d", a.ArticleID))+`">revision `+fmt.Sprintf("%d", a.ArticleID)+`</a>
 | 
				
			||||||
				|
 | 
									|
 | 
				
			||||||
				<a href="`+template.HTMLEscapeString(this.opts.ExternalBaseURL+`diff/parent/`+fmt.Sprintf("%d", a.ID))+`">diff to previous</a>
 | 
									<a href="`+template.HTMLEscapeString(this.opts.ExternalBaseURL+`diff/parent/`+fmt.Sprintf("%d", a.ArticleID))+`">diff to previous</a>
 | 
				
			||||||
			`) + `</description>
 | 
								`) + `</description>
 | 
				
			||||||
		</item>
 | 
							</item>
 | 
				
			||||||
		`
 | 
							`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"database/sql"
 | 
						"database/sql"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
@@ -37,25 +37,46 @@ func (this *WikiServer) routeRecentChanges(w http.ResponseWriter, r *http.Reques
 | 
				
			|||||||
	pto.CurrentPageName = "Recent Changes"
 | 
						pto.CurrentPageName = "Recent Changes"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	content := `<h2>Recent Changes</h2><br>` +
 | 
						content := `<h2>Recent Changes</h2><br>` +
 | 
				
			||||||
		`<em>Showing up to ` + fmt.Sprintf("%d", this.opts.RecentChanges) + ` changes.</em><br>` +
 | 
							`<em>Showing up to ` + fmt.Sprintf("%d", this.opts.RecentChanges) + ` changes.</em><br><br>` +
 | 
				
			||||||
		`<table>`
 | 
							`<div style="display:inline-block;">` +
 | 
				
			||||||
 | 
							`<table class="ti">` +
 | 
				
			||||||
 | 
							`<tr><td>Page</td><td>Actions</td><td>Time</td><td>Author</td></tr>`
 | 
				
			||||||
	for _, rev := range recents {
 | 
						for _, rev := range recents {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							diffHtml := ""
 | 
				
			||||||
 | 
							diffRev, err := this.db.GetNextOldestRevision(int(rev.ArticleID))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								diffHtml = `[new]`
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								diffHtml = `<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`diff/`+fmt.Sprintf("%d/%d", diffRev, rev.ArticleID)) + `">diff</a>`
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							classAttr := ""
 | 
				
			||||||
 | 
							if rev.IsDeleted {
 | 
				
			||||||
 | 
								classAttr = `class="deleted"`
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		content += `<tr>` +
 | 
							content += `<tr>` +
 | 
				
			||||||
			`<td><a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.QueryEscape(rev.Title)) + `">` + template.HTMLEscapeString(rev.Title) + `</a>` +
 | 
								`<td><a ` + classAttr + ` href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`view/`+url.PathEscape(rev.Title)) + `">` + template.HTMLEscapeString(rev.Title) + `</a></td>` +
 | 
				
			||||||
			` [<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", rev.ID)) + `">a</a>]` +
 | 
								`<td>` +
 | 
				
			||||||
 | 
								`<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`archive/`+fmt.Sprintf("%d", rev.ArticleID)) + `">rev</a>   ` +
 | 
				
			||||||
 | 
								diffHtml +
 | 
				
			||||||
			`</td>` +
 | 
								`</td>` +
 | 
				
			||||||
			`<td>` + this.formatTimestamp(rev.Modified) + ` by ` + template.HTMLEscapeString(rev.Author) + `</td>` +
 | 
								`</td>` +
 | 
				
			||||||
 | 
								`<td>` + string(this.formatTimestamp(rev.Modified)) + `</td>` +
 | 
				
			||||||
 | 
								`<td>` + template.HTMLEscapeString(rev.Author) + `</td>` +
 | 
				
			||||||
			`</tr>`
 | 
								`</tr>`
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	content += `<tr><td>`
 | 
						content += `</table>`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if pageNum > 1 {
 | 
						if pageNum > 1 {
 | 
				
			||||||
		content += `<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`recent/`+fmt.Sprintf("%d", pageNum-1)) + `">« Newer</a>`
 | 
							content += `<span style="float:left;"><a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`recent/`+fmt.Sprintf("%d", pageNum-1)) + `">« Newer</a></span>`
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	content += `</td><td></td><td style="text-align:right;">`
 | 
					
 | 
				
			||||||
	if pageNum < maxPage {
 | 
						if pageNum < maxPage {
 | 
				
			||||||
		content += `<a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`recent/`+fmt.Sprintf("%d", pageNum+1)) + `">Older »</a>`
 | 
							content += `<span style="float:right;"><a href="` + template.HTMLEscapeString(this.opts.ExpectBaseURL+`recent/`+fmt.Sprintf("%d", pageNum+1)) + `">Older »</a></span>`
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	content += `</td></tr></table>`
 | 
						content += `</div>`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	pto.Content = template.HTML(content)
 | 
						pto.Content = template.HTML(content)
 | 
				
			||||||
	this.servePageResponse(w, r, pto)
 | 
						this.servePageResponse(w, r, pto)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								rView.go
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								rView.go
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"database/sql"
 | 
						"database/sql"
 | 
				
			||||||
@@ -15,22 +15,27 @@ func (this *WikiServer) routeView(w http.ResponseWriter, r *http.Request, articl
 | 
				
			|||||||
			// If this was an old link, it might not be present.
 | 
								// If this was an old link, it might not be present.
 | 
				
			||||||
			// Redirect if possible
 | 
								// Redirect if possible
 | 
				
			||||||
			if len(articleTitle) > 0 && articleTitle[len(articleTitle)-1] == '/' {
 | 
								if len(articleTitle) > 0 && articleTitle[len(articleTitle)-1] == '/' {
 | 
				
			||||||
				this.serveRedirect(w, this.opts.ExpectBaseURL+"view/"+url.QueryEscape(articleTitle[0:len(articleTitle)-1]))
 | 
									this.serveRedirect(w, this.opts.ExpectBaseURL+"view/"+url.PathEscape(articleTitle[0:len(articleTitle)-1]))
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			this.serveErrorHTMLMessage(w, this.noSuchArticleError(articleTitle))
 | 
								this.serveNoSuchArticle(w, articleTitle)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		this.serveErrorMessage(w, err)
 | 
							this.serveErrorMessage(w, err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if string(a.Body) == bbcodeIsDeleted {
 | 
				
			||||||
 | 
							this.serveDeleted(w, articleTitle)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	pto := DefaultPageTemplateOptions(this.opts)
 | 
						pto := DefaultPageTemplateOptions(this.opts)
 | 
				
			||||||
	pto.CurrentPageName = articleTitle
 | 
						pto.CurrentPageName = articleTitle
 | 
				
			||||||
	pto.CurrentPageIsArticle = true
 | 
						pto.CurrentPageIsArticle = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	bcr := NewBBCodeRenderer(this.opts.ExpectBaseURL)
 | 
						bcr := this.GetBBCodeRenderer()
 | 
				
			||||||
	pto.Content = bcr.RenderHTML(string(a.Body))
 | 
						pto.Content = bcr.RenderHTML(string(a.Body))
 | 
				
			||||||
	pto.LoadCodeResources = bcr.CodePresent
 | 
						pto.LoadCodeResources = bcr.CodePresent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								regex.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								regex.go
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
package yatwiki3
 | 
					package yatwiki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,11 @@ td      {padding:0px 10px;}
 | 
				
			|||||||
.spoiler{color:black;background-color:black;}
 | 
					.spoiler{color:black;background-color:black;}
 | 
				
			||||||
.spoiler:hover{color:white;}
 | 
					.spoiler:hover{color:white;}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a.deleted {
 | 
				
			||||||
 | 
						color:red;
 | 
				
			||||||
 | 
						text-decoration:line-through;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.imgur {
 | 
					.imgur {
 | 
				
			||||||
	border:1px solid white;
 | 
						border:1px solid white;
 | 
				
			||||||
	width:90px;
 | 
						width:90px;
 | 
				
			||||||
@@ -112,16 +117,6 @@ fieldset legend {
 | 
				
			|||||||
	display:inline-block;
 | 
						display:inline-block;
 | 
				
			||||||
	width:16px;height:16px;
 | 
						width:16px;height:16px;
 | 
				
			||||||
	vertical-align:text-bottom;
 | 
						vertical-align:text-bottom;
 | 
				
			||||||
	background-repeat:no-repeat;
 | 
					 | 
				
			||||||
	background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAwCAYAAABwrHhvAAAGS0lEQVRYw52Y32sVVxDHl4JI+2ApwUj7YCmC0ioJIVQI0YpSUwlRoxQlInmQoBEjBpsI/gR/Iv5Co9GYqKiXoCCiL0pBRUViNBA3EBAlBIwvvvsPnJ7P4cwyd3fv7k0XvnfnnvnO7OyPc2bOBMaY4O7duwncunUrvH37tgHIekz+YyvQ/DSg13yB+ykUCkXo7+8PBwYGzOTkpAMyY1euXHH/OcPTjm7cuBHxNS5evBidL1++HKYGcO3atQgXLlwIz507Z8bHx83nz58dkBk7deqUeffunTl27JiBqx1dvXrVfPz4MYF9+/aZ169fOyAfOXIkTARw6dIlh+PHj4eHDh1yZKJGBsiMIXOhnTt3GvjaUU9Pjws0jtbWVhe0gP+JAE6fPh3s2bMnxPGjR4/M+/fv3UVu3rzpgMwYOuQtW7YYbLQjeTpxnDx50ixZsiQCtokAtm7dGra0tLiLYYTMOxsdHXVAZgwdHGRstKPDhw9HjzoL8BIBrFy5MiIg846HhoaKwFicpx3ZJ2iePXuWC3iJANasWRPW1tYasHfvXvPkyZNUoBMeNtqRvL48wEsEsGvXrmD79u3m4cOHEe7cuRPNYWStg4uNdsS7vXfvXi5SvwGcbd682QwODkaQOS/zXuvgxgPYuHFj9NFmAV4iADu9grVr17opJuCdy5eMrHVwsdGOGhsbXaB5gJcIwL7bYPny5W6xEeh3iqx1cLHRjpYtW2bOnDmTC3iJALq7u4PFixcXffXr1q0zIyMjDshaBxcb7YgP89WrV7mAlwhg9+7dwerVq8OqqiojgFhXV+eArHVwsdGOmpqawoULF5o8wEsEYI/5FrUWSy3qLBosGmP40+J3j0XYFDkKgl+9rt6iOQaxrbFYkDoLgD2+taj0mAvZ42c/9gMc4ccC4JhhUWHxo7cj0F+87fden8yGPp9/zcrnsdz+NS23/x9EguTtcgB3OheJFzepAei8nQe407k4AX/58iW1MImI8bydhXhOL4Xr16+HVEr37993QUtdoW+gaC3XeTsL8fU8DX19fWFvb6/58OGDgw0mKk70DUw7n6fl9DjOnz8fsuqNjY0VgYC4gebm5uQriOfz+JGV0zVOnDgRpt0Mj59x9KkfYTyfU2IJOLJyuuDAgQNhV1eXefr0aRF4BYyjLzkNS+VzDorRrJwOOjs7w23btpkHDx4UgcKUcfSZ60BaPuewa35uTpd5/vjx4yKe1Jfx+jE1gLR8ziEyX7EsJkePHi0KQG8+mHKc37x543L/pk2bwrJWwrR8znQRmTqAi7CgIGsn8c0Hd08W1V97bgB5+Vz2Bpx5rNpJ2uaDlD2tXJCXz7kjSnHO8ZzOneqFqpw7TwSg8rnUA20W/1jssPjb1wroq1NqgQVet9RD1xFNFn95v3CqtH3kxNcD3/icP8fn8UU+sJ98np8Fp0QtEHh9pbef523n+foA+++EGNlOTU2VhC3BW0EWJ69HUKovkBuANW6VxQS5FE96CfGeArMFMJYZwKdPn1JhHY5LFYxciie9BDlLX0FKePoNmQHYaBOwi858NiDiEJmxNK70E3TSYYxeQ1mzYGJiIrArXb2tUlpttH0WBYt/efQyr5EZ87o+uNhgG09i/KfXUPY0tA7rbf52yyfVCx8O+78XL15Ed4XMGDo4cLHBVvoKGnnrf1EAZ8+erWZtZ+0eHh7O3d/DgYsNtrpnoKH7B6U6aIwHtlIJDh48OGDTpesB8NGV2tujgwMXG2x1b0FD9w+YCVKUajAejI6OOuzfv7+7o6Njkkddam+PDg5csSvnMUvnTTpuAsaCt2/fRrCO6ykeSu3t0cHRNuV+bLoDJ+B/YIUI7e3tvZRO5H+mHgUGQGYMHRxtU87FdQdOunAyYwKbaiOsX78+pA9UKBTcB0eLDSAzhg6Otsm6sHTf9EyRKknOwcuXLyPYVPpVphvVjL3YEECW6QlH22QFkDZDGNuwYUMo5+D58+cONsPNqqmpcQXnqlWrJtra2jpFh8wYOjhwRZcVQNoMiXfXnBOfMqsrKipWNDQ09Fp5RUp/YIUNomf27NlLfU6f523n+31/vc/5f6TYNvqaoN7XBPQifnPXlb2+rwdkf1/pawDpD8xV/YGZMRs5Znp9paondH9hjqopZojRf/M9B2Tz737/AAAAAElFTkSuQmCC);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.sprite.hm { background-position:0px 0px;}
 | 
					 | 
				
			||||||
.sprite.hs { background-position:0px -16px;}
 | 
					 | 
				
			||||||
.sprite.sp { background-position:0px -32px;}
 | 
					 | 
				
			||||||
.sprite.nw { background-position:-16px 0px;}
 | 
					 | 
				
			||||||
.sprite.ed { background-position:-16px -16px;}
 | 
					 | 
				
			||||||
.sprite.rn { background-position:-16px -32px;}
 | 
					 | 
				
			||||||
.sprite.no {
 | 
					 | 
				
			||||||
	background:none;
 | 
						background:none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
.sep {
 | 
					.sep {
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Reference in New Issue
	
	Block a user