2017-08-13 02:41:14 +00:00
|
|
|
package archive
|
|
|
|
|
|
|
|
import (
|
2017-08-13 04:15:03 +00:00
|
|
|
"bufio"
|
2017-08-13 02:41:14 +00:00
|
|
|
"fmt"
|
2017-12-09 22:57:59 +00:00
|
|
|
"html"
|
2017-08-13 03:47:03 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"math"
|
2017-08-13 02:41:14 +00:00
|
|
|
"net/http"
|
2017-12-09 23:50:11 +00:00
|
|
|
"net/url"
|
2017-08-13 04:15:03 +00:00
|
|
|
"os"
|
|
|
|
"regexp"
|
2017-08-13 03:47:03 +00:00
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
pageNotSet int = -1
|
2017-08-13 02:41:14 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type ArchiveState struct {
|
|
|
|
svr *ArchiveServer
|
|
|
|
log *LogSource
|
|
|
|
logBestSlug string
|
|
|
|
query string
|
|
|
|
queryIsRegex bool
|
|
|
|
ym YearMonth
|
|
|
|
page int
|
|
|
|
highestPage int
|
|
|
|
}
|
|
|
|
|
2017-08-13 03:04:40 +00:00
|
|
|
func NewArchiveState(svr *ArchiveServer) *ArchiveState {
|
2017-08-13 03:47:03 +00:00
|
|
|
return &ArchiveState{
|
|
|
|
svr: svr,
|
|
|
|
page: pageNotSet,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-09 23:50:19 +00:00
|
|
|
func (this *ArchiveState) URL() string {
|
|
|
|
if len(this.query) > 0 {
|
|
|
|
if this.queryIsRegex {
|
|
|
|
return fmt.Sprintf(`/%s/rx/%s`, this.logBestSlug, url.QueryEscape(this.query))
|
|
|
|
} else {
|
|
|
|
return fmt.Sprintf(`/%s/search/%s`, this.logBestSlug, url.QueryEscape(this.query))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if this.page == pageNotSet {
|
|
|
|
return fmt.Sprintf(`/%s/%s/%s`, this.logBestSlug, this.ym.Year, this.ym.Month)
|
|
|
|
} else {
|
|
|
|
return fmt.Sprintf(`/%s/%s/%s/page-%d`, this.logBestSlug, this.ym.Year, this.ym.Month, this.page)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-13 03:47:03 +00:00
|
|
|
func (this *ArchiveState) selectSource(log *LogSource, slug string) {
|
|
|
|
this.log = log
|
|
|
|
this.logBestSlug = slug
|
2017-08-13 03:04:40 +00:00
|
|
|
}
|
|
|
|
|
2017-08-13 03:47:03 +00:00
|
|
|
// renderView renders a single page of log entries.
|
|
|
|
// - Mandatory: log, ym
|
|
|
|
// - Optional: page
|
|
|
|
func (this *ArchiveState) renderView(w http.ResponseWriter) {
|
|
|
|
fname, err := this.svr.LogFile(this.log, this.ym)
|
2017-08-13 02:41:14 +00:00
|
|
|
if err != nil {
|
2017-08-13 03:47:03 +00:00
|
|
|
this.renderError(w, err.Error())
|
|
|
|
return
|
2017-08-13 02:41:14 +00:00
|
|
|
}
|
|
|
|
|
2017-08-13 03:47:03 +00:00
|
|
|
fc, err := ioutil.ReadFile(fname)
|
|
|
|
if err != nil {
|
|
|
|
this.renderError(w, err.Error())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
lines := strings.Split(string(fc), "\n")
|
|
|
|
|
2017-12-10 00:32:35 +00:00
|
|
|
// Work around #newpage: if the last line is blank, skip
|
|
|
|
if len(lines) > 0 && len(lines[len(lines)-1]) == 0 {
|
|
|
|
lines = lines[:len(lines)-1]
|
|
|
|
}
|
|
|
|
|
2017-08-13 03:47:03 +00:00
|
|
|
this.highestPage = int(math.Ceil(float64(len(lines))/float64(this.svr.cfg.LinesPerPage))) - 1
|
|
|
|
if this.page == pageNotSet || this.page > this.highestPage {
|
|
|
|
this.page = this.highestPage
|
|
|
|
}
|
|
|
|
if this.page < 0 {
|
|
|
|
this.page = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
startLine := this.svr.cfg.LinesPerPage * this.page
|
|
|
|
endLine := this.svr.cfg.LinesPerPage * (this.page + 1)
|
|
|
|
if endLine > len(lines) {
|
|
|
|
endLine = len(lines)
|
|
|
|
}
|
|
|
|
|
|
|
|
output := ""
|
|
|
|
for i := startLine; i < endLine; i += 1 {
|
2017-12-10 00:53:20 +00:00
|
|
|
output += html.EscapeString(lines[i]) + "\n"
|
2017-08-13 03:47:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.renderTemplate(w, []byte(output))
|
2017-08-13 03:04:40 +00:00
|
|
|
}
|
|
|
|
|
2017-08-13 04:15:03 +00:00
|
|
|
// renderSearch renders the search results.
|
|
|
|
// - Mandatory: log, query, queryIsRegex
|
|
|
|
func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
|
|
|
|
|
|
|
|
var matcher func(line string) bool
|
|
|
|
if this.queryIsRegex {
|
|
|
|
rx, err := regexp.Compile(`(?i)` + this.query)
|
|
|
|
if err != nil {
|
|
|
|
this.renderError(w, "Invalid regular expression "+this.query+" ("+err.Error()+")")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
matcher = rx.MatchString
|
|
|
|
} else {
|
|
|
|
queryLower := strings.ToLower(this.query)
|
|
|
|
matcher = func(line string) bool {
|
|
|
|
return strings.Contains(strings.ToLower(line), queryLower)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.renderTemplateHead(w)
|
|
|
|
totalResults := 0
|
|
|
|
w.Write([]byte(`<ul>`))
|
|
|
|
|
|
|
|
limit := this.log.LatestDate().Next() // one off the end
|
|
|
|
for ympair := this.log.EarliestDate(); !ympair.Equals(limit); ympair = ympair.Next() {
|
|
|
|
|
|
|
|
fname, err := this.svr.LogFile(this.log, ympair)
|
|
|
|
if err != nil {
|
|
|
|
continue // no log exists for this ym
|
|
|
|
}
|
|
|
|
|
|
|
|
fh, err := os.Open(fname)
|
|
|
|
if err != nil {
|
|
|
|
continue // can't open this log file
|
|
|
|
}
|
|
|
|
|
|
|
|
func() {
|
|
|
|
defer fh.Close()
|
|
|
|
|
|
|
|
scanner := bufio.NewScanner(fh)
|
|
|
|
|
|
|
|
for i := 0; scanner.Scan(); i += 1 {
|
|
|
|
if !matcher(scanner.Text()) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
totalResults += 1
|
|
|
|
|
|
|
|
page := i / this.svr.cfg.LinesPerPage
|
|
|
|
lineNo := i % this.svr.cfg.LinesPerPage
|
|
|
|
url := fmt.Sprintf(`/%s/%d/%d/page-%d#line-%d`, this.logBestSlug, ympair.Year, ympair.Month, page, lineNo)
|
|
|
|
|
2017-12-09 23:08:22 +00:00
|
|
|
w.Write([]byte(`<li><a href="` + html.EscapeString(url) + `">»</a> ` + html.EscapeString(scanner.Text()) + `</li>`))
|
2017-08-13 04:15:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Write([]byte(`</ul>`))
|
|
|
|
|
|
|
|
if totalResults == 0 {
|
2017-12-09 23:08:22 +00:00
|
|
|
w.Write([]byte(`No search results for "<em>` + html.EscapeString(this.query) + `</em>"`))
|
2017-08-13 04:15:03 +00:00
|
|
|
} else {
|
|
|
|
w.Write([]byte(`<br><em>Found ` + fmt.Sprintf("%d", totalResults) + ` total result(s).</em><br><br>`))
|
|
|
|
}
|
|
|
|
|
|
|
|
this.renderTemplateFoot(w)
|
|
|
|
}
|
|
|
|
|
|
|
|
// renderError renders a plain text string, escaping it for HTML use.
|
2017-08-13 03:04:40 +00:00
|
|
|
func (this *ArchiveState) renderError(w http.ResponseWriter, msg string) {
|
2017-12-09 23:08:22 +00:00
|
|
|
this.renderTemplate(w, []byte(html.EscapeString(msg)))
|
2017-08-13 03:04:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (this *ArchiveState) renderTemplate(w http.ResponseWriter, body []byte) {
|
|
|
|
this.renderTemplateHead(w)
|
|
|
|
w.Write(body)
|
|
|
|
this.renderTemplateFoot(w)
|
2017-08-13 02:41:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (this *ArchiveState) renderTemplateHead(w http.ResponseWriter) {
|
|
|
|
w.Header().Set(`Content-Type`, `text/html; charset=UTF-8`)
|
|
|
|
w.Header().Set(`X-UA-Compatible`, `IE=Edge`)
|
|
|
|
w.WriteHeader(200)
|
|
|
|
|
|
|
|
title := `Archives`
|
|
|
|
if this.log != nil {
|
|
|
|
title = this.log.Description + ` Archives`
|
|
|
|
}
|
|
|
|
|
|
|
|
showPageURLs := (this.log != nil && len(this.query) == 0)
|
|
|
|
|
2017-12-10 00:49:46 +00:00
|
|
|
latestUrl := `/`
|
|
|
|
if this.log != nil {
|
|
|
|
latestUrl = fmt.Sprintf(`/%s/%d/%d`, url.PathEscape(this.logBestSlug), this.log.LatestDate().Year, this.log.LatestDate().Month)
|
|
|
|
}
|
|
|
|
|
2017-08-13 02:41:14 +00:00
|
|
|
w.Write([]byte(`<!DOCTYPE html>
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<meta charset="utf-8">
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
2017-12-09 23:08:22 +00:00
|
|
|
<title>` + html.EscapeString(title) + `</title>
|
2017-12-10 00:44:26 +00:00
|
|
|
<link rel="stylesheet" type="text/css" href="/style.css?nonce=` + fmt.Sprintf("%d", this.svr.startup.Unix()) + `">
|
2017-08-13 02:41:14 +00:00
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div class="layout-top nav">
|
|
|
|
|
2017-12-09 23:20:18 +00:00
|
|
|
<div id="menu-container" class="noselect">
|
|
|
|
<div id="tr1"></div>
|
|
|
|
<div id="tr2"></div>
|
|
|
|
<div class="ddmenu">
|
2017-12-10 00:49:46 +00:00
|
|
|
<a href="` + html.EscapeString(latestUrl) + `">Latest</a>
|
2017-12-09 23:20:18 +00:00
|
|
|
<a onclick="fontSize(1);">Font increase</a>
|
|
|
|
<a onclick="fontSize(-1);">Font decrease</a>
|
|
|
|
<a href="/download" onclick="return confirm('Are you sure you want to download a backup?');">Download backup</a>
|
|
|
|
</div>
|
2017-08-13 02:41:14 +00:00
|
|
|
</div>
|
|
|
|
|
2017-12-09 23:19:07 +00:00
|
|
|
<div id="logo" class="layout-pushdown"><svg viewBox="0 0 24 24">
|
2017-12-09 23:03:08 +00:00
|
|
|
<path fill="#000000" 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" />
|
2017-12-09 23:19:07 +00:00
|
|
|
</svg></div>
|
2017-08-13 02:41:14 +00:00
|
|
|
|
|
|
|
<span class="area-nav">
|
|
|
|
|
2017-12-09 23:50:11 +00:00
|
|
|
<select onchange="window.location.pathname = this.value;">
|
2017-12-10 00:25:34 +00:00
|
|
|
`))
|
2017-08-13 02:41:14 +00:00
|
|
|
|
|
|
|
for i, h := range this.svr.cfg.Logs {
|
|
|
|
slug, _ := this.svr.bestSlugFor(&this.svr.cfg.Logs[i])
|
|
|
|
current := (this.log == &this.svr.cfg.Logs[i])
|
|
|
|
|
2017-12-09 23:50:11 +00:00
|
|
|
targetUri := `/` + url.PathEscape(slug) + `/`
|
|
|
|
if len(this.query) > 0 {
|
|
|
|
if this.queryIsRegex {
|
|
|
|
targetUri += `rx/` + url.PathEscape(this.query)
|
|
|
|
} else {
|
|
|
|
targetUri += `search/` + url.PathEscape(this.query)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
targetUri += fmt.Sprintf(`%d/%d`, h.LatestDate().Year, h.LatestDate().Month)
|
|
|
|
}
|
|
|
|
|
2017-12-10 00:25:34 +00:00
|
|
|
w.Write([]byte(fmt.Sprintf("\t\t\t\t\t<option value=\"%s\"%s>%s</option>\n", html.EscapeString(targetUri), attr(current, " selected"), html.EscapeString(h.Description))))
|
2017-08-13 02:41:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
w.Write([]byte(`
|
2017-12-09 23:50:11 +00:00
|
|
|
</select>
|
2017-08-13 02:41:14 +00:00
|
|
|
`))
|
|
|
|
|
|
|
|
if showPageURLs {
|
|
|
|
w.Write([]byte(`
|
2017-12-09 23:50:11 +00:00
|
|
|
<select onchange="window.location.pathname = this.value;">
|
2017-12-10 00:25:34 +00:00
|
|
|
`))
|
2017-08-13 02:41:14 +00:00
|
|
|
|
|
|
|
// Generate month dropdown options
|
|
|
|
|
|
|
|
lastY := -1
|
2017-08-13 03:47:03 +00:00
|
|
|
limit := this.log.LatestDate().Next() // one off the end
|
|
|
|
for ympair := this.log.EarliestDate(); !ympair.Equals(limit); ympair = ympair.Next() {
|
2017-08-13 02:41:14 +00:00
|
|
|
if ympair.Year != lastY {
|
|
|
|
if lastY != -1 {
|
2017-12-10 00:27:40 +00:00
|
|
|
w.Write([]byte("\t\t\t\t\t</optgroup>\n"))
|
2017-08-13 02:41:14 +00:00
|
|
|
}
|
2017-12-10 00:27:40 +00:00
|
|
|
w.Write([]byte(fmt.Sprintf("\t\t\t\t\t<optgroup label=\"%d\">\n", ympair.Year)))
|
2017-08-13 02:41:14 +00:00
|
|
|
lastY = ympair.Year
|
|
|
|
}
|
|
|
|
|
2017-12-09 23:50:11 +00:00
|
|
|
targetUri := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, ympair.Year, ympair.Month)
|
|
|
|
|
2017-12-10 00:27:40 +00:00
|
|
|
w.Write([]byte(fmt.Sprintf("\t\t\t\t\t\t<option value=\"%s\"%s>%s</option>\n", html.EscapeString(targetUri), attr(ympair.Equals(this.ym), " selected"), html.EscapeString(ympair.Month.String()))))
|
2017-12-09 23:50:11 +00:00
|
|
|
}
|
|
|
|
if lastY != -1 {
|
2017-12-10 00:27:40 +00:00
|
|
|
w.Write([]byte("\t\t\t\t\t</optgroup>\n"))
|
2017-08-13 02:41:14 +00:00
|
|
|
}
|
2017-12-09 23:50:11 +00:00
|
|
|
w.Write([]byte(`
|
|
|
|
</select>
|
|
|
|
|
|
|
|
<div class="mini-separator layout-pushdown"></div>
|
|
|
|
`))
|
2017-08-13 02:41:14 +00:00
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
pageBase := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, this.ym.Year, this.ym.Month)
|
|
|
|
|
|
|
|
previousPage := this.page - 1
|
|
|
|
if previousPage < 0 {
|
|
|
|
previousPage = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
nextPage := this.page + 1
|
|
|
|
if nextPage > this.highestPage {
|
|
|
|
nextPage = this.highestPage
|
|
|
|
}
|
|
|
|
|
2017-12-09 22:57:59 +00:00
|
|
|
prevMonth := this.ym.Prev() // FIXME check bounds
|
|
|
|
prevMonthLink := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, prevMonth.Year, prevMonth.Month)
|
|
|
|
nextMonth := this.ym.Next() // FIXME check bounds
|
|
|
|
nextMonthLink := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, nextMonth.Year, nextMonth.Month)
|
|
|
|
|
2017-08-13 02:41:14 +00:00
|
|
|
w.Write([]byte(`
|
2017-12-09 23:50:11 +00:00
|
|
|
<a style="display:none;" id="monthprev" accesskey="j" href="` + html.EscapeString(prevMonthLink) + `">
|
|
|
|
<a style="display:none;" id="monthnext" accesskey="k" href="` + html.EscapeString(nextMonthLink) + `">
|
|
|
|
|
|
|
|
<a class="btn" href="` + pageBase + `/page-0">«</a>
|
2017-12-10 00:03:47 +00:00
|
|
|
<a class="btn" accesskey="h" title="Previous page (Alt+H)" href="` + pageBase + `/page-` + fmt.Sprintf("%d", previousPage) + `">‹</a>
|
|
|
|
<span class="current-page">` + fmt.Sprintf("%d", this.page) + `</span>
|
|
|
|
<a class="btn" accesskey="l" title="Next page (Alt+L)" href="` + pageBase + `/page-` + fmt.Sprintf("%d", nextPage) + `">›</a>
|
2017-12-09 23:50:11 +00:00
|
|
|
<a class="btn" href="` + pageBase + `">»</a>
|
2017-08-13 02:41:14 +00:00
|
|
|
`))
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Write([]byte(`
|
2017-12-10 00:03:47 +00:00
|
|
|
</span>
|
|
|
|
|
|
|
|
<span class="area-search">
|
|
|
|
<form method="GET">
|
|
|
|
<input type="hidden" name="h" value="` + html.EscapeString(this.logBestSlug) + `">
|
|
|
|
<input type="text" id="searchbox" name="q" value="` + html.EscapeString(this.query) + `" placeholder="Search..." accesskey="m" title="Search (Alt+M)">
|
|
|
|
<input type="submit" value="»">
|
|
|
|
<input type="checkbox" class="layout-pushdown" name="rx" value="1" title="Regular Expression" ` + attr(this.queryIsRegex, "checked") + `>
|
|
|
|
</form>
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</div>
|
2017-08-13 02:41:14 +00:00
|
|
|
|
2017-12-10 00:53:20 +00:00
|
|
|
<div class="layout-body" id="chatarea">`,
|
|
|
|
))
|
2017-08-13 02:41:14 +00:00
|
|
|
|
|
|
|
// Header ends
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *ArchiveState) renderTemplateFoot(w http.ResponseWriter) {
|
2017-12-10 00:53:20 +00:00
|
|
|
w.Write([]byte(`</div>
|
|
|
|
|
2017-08-13 03:04:40 +00:00
|
|
|
<script type="text/javascript" src="/archive.js?nonce=` + fmt.Sprintf("%d", this.svr.startup.Unix()) + `"></script>
|
2017-08-13 02:41:14 +00:00
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
`))
|
|
|
|
}
|