package yatwiki3 import ( "database/sql" "errors" "fmt" "html/template" "math/rand" "net/http" "net/url" "regexp" "strconv" "strings" "time" ) type WikiServer struct { db *WikiDB opts *ServerOptions pageTmp *template.Template loc *time.Location rxDiff *regexp.Regexp bans []*regexp.Regexp } func NewWikiServer(opts *ServerOptions) (*WikiServer, error) { wdb, err := NewWikiDB(opts.DBFilePath, opts.GzipCompressionLevel) if err != nil { return nil, err } tr, err := wdb.TotalRevisions() if (err == nil && tr == 0) || err == sql.ErrNoRows { err := wdb.SaveArticle(opts.DefaultPage, `YATWiki3`, "", 0) if err != nil { return nil, err } } tmpl, err := template.New("yatwiki/page").Parse(pageTemplate) if err != nil { return nil, err } loc, err := time.LoadLocation(opts.Timezone) if err != nil { return nil, err } ws := WikiServer{ db: wdb, opts: opts, pageTmp: tmpl, loc: loc, rxDiff: regexp.MustCompile(`diff/(\d+)/(\d+)`), bans: make([]*regexp.Regexp, 0, len(opts.BannedUserIPRegexes)), } for _, banRx := range opts.BannedUserIPRegexes { rx, err := regexp.Compile(banRx) if err != nil { return nil, err } ws.bans = append(ws.bans, rx) } return &ws, nil } func (this *WikiServer) Close() { this.db.Close() } func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Server", "YATWiki3") if len(this.bans) > 0 { remoteIP := RemoteAddrToIPAddress(r.RemoteAddr) for _, ban := range this.bans { if ban.MatchString(remoteIP) { http.Error(w, "Unauthorised", 403) return } } } if !strings.HasPrefix(r.URL.Path, this.opts.ExpectBaseURL) { http.Error(w, "Bad request", 400) return } remainingPath := r.URL.Path[len(this.opts.ExpectBaseURL):] if r.Method == "GET" { if remainingPath == "wiki.css" { w.Header().Set("Content-Type", "text/css") content, _ := wikiCssBytes() w.Write(content) return } else if remainingPath == "highlight.js" { w.Header().Set("Content-Type", "application/javascript") content, _ := highlightJsBytes() w.Write(content) return } else if remainingPath == "favicon.ico" && len(this.opts.FaviconFilePath) > 0 { w.Header().Set("Content-Type", "image/x-icon") http.ServeFile(w, r, this.opts.FaviconFilePath) return } else if remainingPath == "download-database" { w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Disposition", `attachment; filename="database-`+fmt.Sprintf("%d", time.Now().Unix())+`.db"`) http.ServeFile(w, r, this.opts.DBFilePath) return } else if remainingPath == "formatting" { this.routeFormatting(w, r) return } else if remainingPath == "index" { this.routeIndex(w, r) return } else if remainingPath == "rss/changes" { this.routeRecentChangesRSS(w, r) return } else if remainingPath == "" { this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.QueryEscape(this.opts.DefaultPage)) return } else if remainingPath == "random" { titles, err := this.db.ListTitles() if err != nil { this.serveInternalError(w, r, err) return } chosenArticle := titles[rand.Intn(len(titles))] this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.QueryEscape(chosenArticle)) return } else if strings.HasPrefix(remainingPath, "view/") { articleTitle, err := url.QueryUnescape(remainingPath[len("view/"):]) if err != nil { this.serveErrorMessage(w, err) return } this.routeView(w, r, articleTitle) return } else if strings.HasPrefix(remainingPath, "modify/") { articleTitle, err := url.QueryUnescape(remainingPath[len("modify/"):]) if err != nil { this.serveErrorMessage(w, err) return } this.routeModify(w, r, articleTitle) return } else if strings.HasPrefix(remainingPath, "history/") { articleTitle, err := url.QueryUnescape(remainingPath[len("history/"):]) if err != nil { this.serveErrorMessage(w, err) return } this.routeHistory(w, r, articleTitle) return } else if strings.HasPrefix(remainingPath, "raw/") { revId, err := strconv.Atoi(remainingPath[len("raw/"):]) if err != nil { this.serveErrorMessage(w, err) return } this.routeRawView(w, r, revId) return } else if strings.HasPrefix(remainingPath, "archive/") { revId, err := strconv.Atoi(remainingPath[len("archive/"):]) if err != nil { this.serveErrorMessage(w, err) return } this.routeArchive(w, r, revId) return } else if strings.HasPrefix(remainingPath, "recent/") { pageNum, err := strconv.Atoi(remainingPath[len("recent/"):]) if err != nil { this.serveErrorMessage(w, err) return } this.routeRecentChanges(w, r, pageNum) return } else if remainingPath == "diff" { this.serveRedirect(w, this.opts.ExpectBaseURL+`diff/`+r.URL.Query().Get("f")+`/`+r.URL.Query().Get("t")) return } else if strings.HasPrefix(remainingPath, "diff/parent/") { toRev, err := strconv.Atoi(remainingPath[len("diff/parent/"):]) if err != nil { this.serveErrorMessage(w, err) return } fromRev, err := this.db.GetNextOldestRevision(toRev) if err != nil { this.serveErrorMessage(w, err) return } this.serveRedirect(w, this.opts.ExpectBaseURL+`diff/`+fmt.Sprintf("%d/%d", fromRev, toRev)) return } else if match := this.rxDiff.FindStringSubmatch(remainingPath); len(match) == 3 { fromRev, err := strconv.Atoi(match[1]) if err != nil { this.serveErrorMessage(w, err) return } toRev, err := strconv.Atoi(match[2]) if err != nil { this.serveErrorMessage(w, err) return } this.routeDiff(w, r, fromRev, toRev) return } } else if r.Method == "POST" { if r.URL.Path == this.opts.ExpectBaseURL+"save" { err := r.ParseForm() if err != nil { this.serveErrorMessage(w, err) return } title := r.Form.Get("pname") body := r.Form.Get("content") expectRev, err := strconv.Atoi(r.Form.Get("baserev")) if err != nil { this.serveErrorMessage(w, err) return } err = this.db.SaveArticle(title, Author(r), body, int64(expectRev)) if err != nil { this.serveErrorMessage(w, err) return } this.serveRedirect(w, this.opts.ExpectBaseURL+`view/`+url.QueryEscape(title)) return } } // No match? Add 'Page not found' to next session response, and redirect to homepage this.serveErrorMessage(w, errors.New("Page not found")) }