43 Commits

Author SHA1 Message Date
901cb84a1c 3.1.0 changelog 2017-12-10 15:20:27 +13:00
2ae9d52419 bump all versions to 3.1.0 2017-12-10 15:20:22 +13:00
f5d587819f makefile: fix devel target for when build fails 2017-12-10 15:19:33 +13:00
dea0cffc90 statistics page 2017-12-10 15:19:20 +13:00
917b9c1a53 rebuild staticResources.go 2017-12-10 14:21:02 +13:00
758ba9a457 makefile: add "devel" target 2017-12-10 14:19:42 +13:00
d1007bb645 skip redirect when searching (if javascript is available) 2017-12-10 14:19:35 +13:00
35b9973a4a mobile-friendly search results 2017-12-10 14:05:45 +13:00
437e1f60e5 preserve line-selection hash fragment in URL when navigating to a search result 2017-12-10 13:57:32 +13:00
3c8631c0d7 fix search highlighting for previous 2017-12-10 13:57:16 +13:00
3f4d5e2522 preserve consecutive whitespaces in chat messages 2017-12-10 13:53:20 +13:00
a087ad2e7b 'latest' link takes you to the latest for the currently selected hub 2017-12-10 13:49:46 +13:00
a27645772a css: instant on, animate off 2017-12-10 13:44:33 +13:00
86cc1cbb2e add nonce for css 2017-12-10 13:44:26 +13:00
9ebe6ca2fb rebuild staticResources.go 2017-12-10 13:37:38 +13:00
117a5fa51e fix disappearing text in dropdown menu 2017-12-10 13:37:33 +13:00
5f2a1b528d fix spaces in searches turning into plusses 2017-12-10 13:37:22 +13:00
559283566b fix #newpage issues 2017-12-10 13:32:35 +13:00
add69c5c81 pretty up the generated HTML (2) 2017-12-10 13:27:40 +13:00
f261ca0ece pretty up the generated HTML 2017-12-10 13:25:34 +13:00
672f0f606c rebuild staticResources.go 2017-12-10 13:04:00 +13:00
cd1e868daf fix phantom space between dom elements; fix search box being split into two lines in some cases; add more padding in mobile layout 2017-12-10 13:03:47 +13:00
e078383e6d add archiveState.URL() method 2017-12-10 12:50:19 +13:00
8568c4bdc2 replace js state setting, with URL targets in select values ; remove "PCRE" from regex tooltip 2017-12-10 12:50:11 +13:00
7b9aece4d2 use pathEscape / pathUnescape for query/rx URLs (fixes + in regex) 2017-12-10 12:48:24 +13:00
e0ff64cd22 move menucontainer into a single div, prevent text selection on dropdown elements 2017-12-10 12:20:18 +13:00
3f623782b7 use a constant for default font size 2017-12-10 12:19:48 +13:00
92e7064372 inline the toggle_element function 2017-12-10 12:19:35 +13:00
149e226729 move toggleMenu call from html onclick to DOM addEventListener 2017-12-10 12:19:07 +13:00
3c3028b8d1 css: remove unused style block 2017-12-10 12:18:37 +13:00
8a452c0fa6 standardise html-escaping functions 2017-12-10 12:08:22 +13:00
693f541934 rebuild staticResources.go 2017-12-10 12:03:14 +13:00
e6ed43154c replace star png image with inline SVG 2017-12-10 12:03:08 +13:00
533527c890 remove mousetrap.js (APL) in favour of accesskey; add title tags 2017-12-10 11:57:59 +13:00
f28ae17a00 fix legacy index.php still being routable(!) 2017-12-10 11:43:02 +13:00
cd879798e3 bump all versions to 3.0.2 2017-09-06 18:19:57 +12:00
f90a6d1dc6 Added tag release-3.0.1 for changeset 47147713ae1b 2017-09-06 18:19:47 +12:00
ba097e3a9d rebuild staticResources.go 2017-09-06 18:18:25 +12:00
0dd72a0468 bump all versions to 3.0.1 2017-09-06 18:18:17 +12:00
d91513c1ac doc: changelog 2017-09-06 18:17:31 +12:00
957195ca11 convert time specs to strftime 2017-09-06 18:17:25 +12:00
bed15bfb33 track github.com/jehiah/go-strftime (ISC, git-2efbe75) 2017-09-06 18:17:14 +12:00
7641f9d73d Added tag release-3.0.0 for changeset 718fcb17fc0c 2017-08-13 17:14:49 +12:00
17 changed files with 534 additions and 216 deletions

2
.hgtags Normal file
View File

@@ -0,0 +1,2 @@
718fcb17fc0c119591e837d700c752601c392130 release-3.0.0
47147713ae1b5de60d49faeed6e17f962d5deae0 release-3.0.1

View File

@@ -5,6 +5,8 @@ import (
"fmt"
"regexp"
"time"
"github.com/jehiah/go-strftime"
)
var (
@@ -16,7 +18,7 @@ type ArchiveServer struct {
cfg *Config
startup time.Time
rxViewRoot, rxViewPage, rxSearch, rxSearchRx *regexp.Regexp
rxViewRoot, rxViewPage, rxSearch, rxSearchRx, rxStats *regexp.Regexp
}
func NewArchiveServer(cfg *Config) (*ArchiveServer, error) {
@@ -47,6 +49,7 @@ func NewArchiveServer(cfg *Config) (*ArchiveServer, error) {
rxViewPage: regexp.MustCompile(`^/([^/]+)/(\d+)/(\d+)/(?:page-)?(\d+)$`),
rxSearch: regexp.MustCompile(`^/([^/]+)/search/(.*)$`),
rxSearchRx: regexp.MustCompile(`^/([^/]+)/rx/(.*)$`),
rxStats: regexp.MustCompile((`^/([^/]+)/stats/?$`)),
}, nil
}
@@ -57,7 +60,7 @@ func (this *ArchiveServer) LogFile(ls *LogSource, ym YearMonth) (string, error)
for _, fl := range ls.FileLocation {
if fl.StartMonth.Index() <= ymIndex && (fl.EndMonth == nil || fl.EndMonth.Index() >= ymIndex) {
return time.Date(ym.Year, ym.Month, 1, 0, 0, 0, 0, this.timezone).Format(fl.LogFilePath), nil
return strftime.Format(fl.LogFilePath, time.Date(ym.Year, ym.Month, 1, 0, 0, 0, 0, this.timezone)), nil
}
}

View File

@@ -2,14 +2,18 @@ package archive
import (
"bufio"
"errors"
"fmt"
"html/template"
"html"
"io/ioutil"
"math"
"net/http"
"net/url"
"os"
"regexp"
"sort"
"strings"
"time"
)
const (
@@ -22,6 +26,7 @@ type ArchiveState struct {
logBestSlug string
query string
queryIsRegex bool
isStats bool
ym YearMonth
page int
highestPage int
@@ -34,6 +39,30 @@ func NewArchiveState(svr *ArchiveServer) *ArchiveState {
}
}
func (this *ArchiveState) showPageURLs() bool {
return this.log != nil && len(this.query) == 0 && !this.isStats
}
func (this *ArchiveState) URL() string {
if this.isStats {
return fmt.Sprintf(`/%s/stats`, this.logBestSlug)
} else 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)
}
}
}
func (this *ArchiveState) selectSource(log *LogSource, slug string) {
this.log = log
this.logBestSlug = slug
@@ -57,6 +86,11 @@ func (this *ArchiveState) renderView(w http.ResponseWriter) {
lines := strings.Split(string(fc), "\n")
// 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]
}
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
@@ -73,12 +107,136 @@ func (this *ArchiveState) renderView(w http.ResponseWriter) {
output := ""
for i := startLine; i < endLine; i += 1 {
output += template.HTMLEscapeString(lines[i]) + "<br>\n"
output += html.EscapeString(lines[i]) + "\n"
}
this.renderTemplate(w, []byte(output))
}
// parseStatsFor loads user post statistics for a single yearMonth in a single log source.
func (this *ArchiveState) parseStatsFor(ls *LogSource, ym YearMonth, into map[string]int) error {
fname, err := this.svr.LogFile(ls, ym)
if err != nil {
return err
}
fc, err := ioutil.ReadFile(fname)
if err != nil {
return err
}
rxUser := regexp.MustCompile(`(?ms)^[^<\r\n]*<([^>\r\n]+)>.+?$`)
matches := rxUser.FindAllSubmatch(fc, -1)
if matches == nil || len(matches) == 0 {
return errors.New("No matches")
}
for _, match := range matches {
username := string(match[1])
if ct, ok := into[username]; ok {
into[username] = ct + 1
} else {
into[username] = 1
}
}
return nil
}
func (this *ArchiveState) renderStats(w http.ResponseWriter) {
// Lines per year
// Users / posts/year
startTime := time.Now()
yearsToUsersToPostCount := make(map[int]map[string]int, 0)
totalErrors := 0
var lastError error = nil
ym := this.log.EarliestDate()
orderedYears := make([]int, 0)
for {
usersToPostCount, ok := yearsToUsersToPostCount[ym.Year]
if !ok {
usersToPostCount = make(map[string]int)
orderedYears = append(orderedYears, ym.Year)
}
err := this.parseStatsFor(this.log, ym, usersToPostCount)
if err != nil {
//log.Printf("Stats(%s): %s", this.logBestSlug, err.Error())
totalErrors += 1
lastError = err
}
//fmt.Printf("%#v\n", usersToPostCount)
yearsToUsersToPostCount[ym.Year] = usersToPostCount
if ym.Equals(this.log.LatestDate()) {
break
}
ym = ym.Next()
}
ret := make([]byte, 0)
if lastError != nil {
ret = append(ret, []byte(fmt.Sprintf("<em>Got %d errors, including: '%s'</em>\n\n", totalErrors, lastError.Error()))...)
}
//
allUsersExistence := make(map[string]struct{})
for _, usersMap := range yearsToUsersToPostCount {
for username, _ := range usersMap {
allUsersExistence[username] = struct{}{}
}
}
allUsernames := make([]string, 0, len(allUsersExistence))
for username, _ := range allUsersExistence {
allUsernames = append(allUsernames, username)
}
sort.Strings(allUsernames)
//
ret = append(ret, []byte(`<table><thead><tr><th>&nbsp;</th>`)...)
for _, year := range orderedYears {
ret = append(ret, []byte(fmt.Sprintf(`<th>%d</th>`, year))...)
}
ret = append(ret, []byte("</tr><tbody>\n")...)
for _, username := range allUsernames {
ret = append(ret, []byte(fmt.Sprintf(`<tr><td>%s</td>`, html.EscapeString(username)))...)
for _, year := range orderedYears {
usersMap := yearsToUsersToPostCount[year]
posts, _ /*ok*/ := usersMap[username]
ret = append(ret, []byte(fmt.Sprintf(`<td>%d</td>`, posts))...)
}
ret = append(ret, []byte("</tr>\n")...)
}
ret = append(ret, []byte(`</tbody><tfoot><th style="text-align:right;">TOTAL:</th>`)...)
for _, year := range orderedYears {
postsTotalForYear := 0
for _, userPostCount := range yearsToUsersToPostCount[year] {
postsTotalForYear += userPostCount
}
ret = append(ret, []byte(fmt.Sprintf(`<th>%d</th>`, postsTotalForYear))...)
}
duration := time.Now().Sub(startTime)
ret = append(ret, []byte(fmt.Sprintf("</tfoot></table>\n\n<em>%d total users\nStatistics generated in %s</em>", len(allUsernames), duration.String()))...)
this.renderTemplate(w, ret)
}
// renderSearch renders the search results.
// - Mandatory: log, query, queryIsRegex
func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
@@ -101,7 +259,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
this.renderTemplateHead(w)
totalResults := 0
w.Write([]byte(`<ul>`))
w.Write([]byte(`<ul class="search-results">`))
limit := this.log.LatestDate().Next() // one off the end
for ympair := this.log.EarliestDate(); !ympair.Equals(limit); ympair = ympair.Next() {
@@ -131,7 +289,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
lineNo := i % this.svr.cfg.LinesPerPage
url := fmt.Sprintf(`/%s/%d/%d/page-%d#line-%d`, this.logBestSlug, ympair.Year, ympair.Month, page, lineNo)
w.Write([]byte(`<li><a href="` + template.HTMLEscapeString(url) + `">&raquo;</a> ` + template.HTMLEscapeString(scanner.Text()) + `</li>`))
w.Write([]byte(`<li><a href="` + html.EscapeString(url) + `">&raquo;</a> ` + html.EscapeString(scanner.Text()) + `</li>`))
}
}()
@@ -140,7 +298,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
w.Write([]byte(`</ul>`))
if totalResults == 0 {
w.Write([]byte(`No search results for &quot;<em>` + template.HTMLEscapeString(this.query) + `</em>&quot;`))
w.Write([]byte(`No search results for &quot;<em>` + html.EscapeString(this.query) + `</em>&quot;`))
} else {
w.Write([]byte(`<br><em>Found ` + fmt.Sprintf("%d", totalResults) + ` total result(s).</em><br><br>`))
}
@@ -150,7 +308,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
// renderError renders a plain text string, escaping it for HTML use.
func (this *ArchiveState) renderError(w http.ResponseWriter, msg string) {
this.renderTemplate(w, []byte(template.HTMLEscapeString(msg)))
this.renderTemplate(w, []byte(html.EscapeString(msg)))
}
func (this *ArchiveState) renderTemplate(w http.ResponseWriter, body []byte) {
@@ -169,55 +327,70 @@ func (this *ArchiveState) renderTemplateHead(w http.ResponseWriter) {
title = this.log.Description + ` Archives`
}
showPageURLs := (this.log != nil && len(this.query) == 0)
latestUrl := `/`
if this.log != nil {
latestUrl = fmt.Sprintf(`/%s/%d/%d`, url.PathEscape(this.logBestSlug), this.log.LatestDate().Year, this.log.LatestDate().Month)
}
statsUrl := fmt.Sprintf(`/%s/stats`, url.PathEscape(this.logBestSlug))
w.Write([]byte(`<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>` + template.HTMLEscapeString(title) + `</title>
<link rel="stylesheet" type="text/css" href="/style.css">
<title>` + html.EscapeString(title) + `</title>
<link rel="stylesheet" type="text/css" href="/style.css?nonce=` + fmt.Sprintf("%d", this.svr.startup.Unix()) + `">
</head>
<body>
<div class="layout-top nav">
<div id="tr1" style="display:none;"></div>
<div id="tr2" style="display:none;"></div>
<div class="ddmenu" id="spm" style="display:none;">
<a href="/">Latest</a>
<div id="menu-container" class="noselect">
<div id="tr1"></div>
<div id="tr2"></div>
<div class="ddmenu">
<a href="` + html.EscapeString(latestUrl) + `">Latest</a>
<a href="` + html.EscapeString(statsUrl) + `">Statistics</a>
<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>
</div>
<a onclick="toggleMenu();"><div id="logo" class="layout-pushdown"></div></a>
<div id="logo" class="layout-pushdown"><svg viewBox="0 0 24 24">
<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" />
</svg></div>
<span class="area-nav">
<form method="GET" id="frmHub">
<select name="h" id="selHub">
`))
<select onchange="window.location.pathname = this.value;">
`))
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])
w.Write([]byte(`<option value="` + template.HTMLEscapeString(slug) + `" ` + attr(current, "selected") + `>` + template.HTMLEscapeString(h.Description) + `</option>`))
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)
}
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))))
}
w.Write([]byte(`
</select>
</form>
`))
if showPageURLs {
if this.showPageURLs() {
w.Write([]byte(`
<form method="GET">
<input type="hidden" name="h" value="` + template.HTMLEscapeString(this.logBestSlug) + `">
<select id="seldate" onchange="setYM(this);">
`))
<select onchange="window.location.pathname = this.value;">
`))
// Generate month dropdown options
@@ -226,14 +399,24 @@ func (this *ArchiveState) renderTemplateHead(w http.ResponseWriter) {
for ympair := this.log.EarliestDate(); !ympair.Equals(limit); ympair = ympair.Next() {
if ympair.Year != lastY {
if lastY != -1 {
w.Write([]byte(`</optgroup>`))
w.Write([]byte("\t\t\t\t\t</optgroup>\n"))
}
w.Write([]byte(`<optgroup label="` + fmt.Sprintf("%d", ympair.Year) + `">`))
w.Write([]byte(fmt.Sprintf("\t\t\t\t\t<optgroup label=\"%d\">\n", ympair.Year)))
lastY = ympair.Year
}
w.Write([]byte(fmt.Sprintf(`<option value="%d-%d" %s>%s</option>`, ympair.Year, ympair.Month, attr(ympair.Equals(this.ym), "selected"), template.HTMLEscapeString(ympair.Month.String()))))
targetUri := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, ympair.Year, ympair.Month)
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()))))
}
if lastY != -1 {
w.Write([]byte("\t\t\t\t\t</optgroup>\n"))
}
w.Write([]byte(`
</select>
<div class="mini-separator layout-pushdown"></div>
`))
//
@@ -249,51 +432,46 @@ func (this *ArchiveState) renderTemplateHead(w http.ResponseWriter) {
nextPage = this.highestPage
}
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)
w.Write([]byte(`
</optgroup>
<a style="display:none;" id="monthprev" accesskey="j" href="` + html.EscapeString(prevMonthLink) + `">
<a style="display:none;" id="monthnext" accesskey="k" href="` + html.EscapeString(nextMonthLink) + `">
</select>
<input type="hidden" name="y" id="f_y" value="">
<input type="hidden" name="m" id="f_m" value="">
<input type="hidden" name="p" value="0" >
</form>
<div class="mini-separator layout-pushdown"></div>
<a class="btn" href="` + pageBase + `/page-0">&laquo;</a><a
class="btn" id="pgprev" href="` + pageBase + `/page-` + fmt.Sprintf("%d", previousPage) + `">&lsaquo;</a>
` + fmt.Sprintf("%d", this.page) + `
<a class="btn" id="pgnext" href="` + pageBase + `/page-` + fmt.Sprintf("%d", nextPage) + `">&rsaquo;</a><a
class="btn" href="` + pageBase + `">&raquo;</a>
<a class="btn" href="` + pageBase + `/page-0">&laquo;</a>
<a class="btn" accesskey="h" title="Previous page (Alt+H)" href="` + pageBase + `/page-` + fmt.Sprintf("%d", previousPage) + `">&lsaquo;</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) + `">&rsaquo;</a>
<a class="btn" href="` + pageBase + `">&raquo;</a>
`))
}
w.Write([]byte(`
<div class="pad"></div>
</span>
<span class="area-search">
<form method="GET">
<input type="hidden" name="h" value="` + template.HTMLEscapeString(this.logBestSlug) + `">
<input type="text" id="searchbox" name="q" value="` + template.HTMLEscapeString(this.query) + `" placeholder="Search...">
<form method="GET" id="search-form">
<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="&raquo;">
<input type="checkbox" class="layout-pushdown" name="rx" value="1" title="PCRE Regular Expression" ` + attr(this.queryIsRegex, "checked") + `>
<input type="checkbox" class="layout-pushdown" name="rx" value="1" title="Regular Expression" ` + attr(this.queryIsRegex, "checked") + `>
</form>
</span>
</div>
<div class="layout-body" id="chatarea">
`))
<div class="layout-body" id="chatarea">`,
))
// Header ends
}
func (this *ArchiveState) renderTemplateFoot(w http.ResponseWriter) {
w.Write([]byte(`
</div>
w.Write([]byte(`</div>
<script type="text/javascript" src="/archive.js?nonce=` + fmt.Sprintf("%d", this.svr.startup.Unix()) + `"></script>
</body>
</html>

View File

@@ -2,7 +2,7 @@
# Makefile for archive
#
VERSION:=3.0.0
VERSION:=3.1.0
SOURCES:=Makefile \
static \
@@ -15,7 +15,7 @@ GOFLAGS := -ldflags='-s -w -X code.ivysaur.me/archive.SERVER_VERSION=archive/${V
# Phony targets
#
.PHONY: all dist clean
.PHONY: all dist clean devel
all: build/linux64/archive-server build/win32/archive-server.exe
@@ -29,6 +29,9 @@ clean:
if [ -d ./build ] ; then rm -r ./build ; fi
if [ -f ./archive ] ; then rm ./archive ; fi
devel: staticResources.go
( cd cmd/archive-server && go build && ./archive-server -listen "127.0.0.1:8000" )
#
# Generated files
#

View File

@@ -121,7 +121,7 @@ func (this *ArchiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} else if matches := this.rxSearch.FindStringSubmatch(r.URL.Path); len(matches) > 0 {
if ls := this.lookupSource(matches[1]); ls != nil {
arc.selectSource(ls, matches[1])
arc.query, _ = url.QueryUnescape(matches[2])
arc.query, _ = url.PathUnescape(matches[2])
arc.queryIsRegex = false
arc.renderSearch(w)
@@ -132,13 +132,24 @@ func (this *ArchiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} else if matches := this.rxSearchRx.FindStringSubmatch(r.URL.Path); len(matches) > 0 {
if ls := this.lookupSource(matches[1]); ls != nil {
arc.selectSource(ls, matches[1])
arc.query, _ = url.QueryUnescape(matches[2])
arc.query, _ = url.PathUnescape(matches[2])
arc.queryIsRegex = true
arc.renderSearch(w)
} else {
arc.renderError(w, fmt.Sprintf("Unknown source '%s'", matches[1]))
}
} else if matches := this.rxStats.FindStringSubmatch(r.URL.Path); len(matches) > 0 {
if ls := this.lookupSource(matches[1]); ls != nil {
arc.selectSource(ls, matches[1])
arc.isStats = true
arc.renderStats(w)
} else {
arc.renderError(w, fmt.Sprintf("Unknown source '%s'", matches[1]))
}
} else {
arc.renderError(w, "Unknown route.")
@@ -157,9 +168,9 @@ func (this *ArchiveServer) legacyRoute(w http.ResponseWriter, r *http.Request) {
if u.hasGet("q") {
if u.hasGet("rx") {
u.redirectf(`/%d/rx/%s`, hubid, url.QueryEscape(u.get("q")))
u.redirectf(`/%d/rx/%s`, hubid, url.PathEscape(u.get("q")))
} else {
u.redirectf(`/%d/search/%s`, hubid, url.QueryEscape(u.get("q")))
u.redirectf(`/%d/search/%s`, hubid, url.PathEscape(u.get("q")))
}
} else if u.hasGet("y") && u.hasGet("m") {

View File

@@ -27,6 +27,14 @@ func (ym YearMonth) Next() YearMonth {
}
}
func (ym YearMonth) Prev() YearMonth {
if ym.Month == time.January {
return YearMonth{Year: ym.Year - 1, Month: time.December}
} else {
return YearMonth{Year: ym.Year, Month: ym.Month - 1}
}
}
// Index returns a single int that can be used to compare this YearMonth with
// other YearMonth objects.
func (ym YearMonth) Index() int {

View File

@@ -6,6 +6,26 @@ Written in Golang, PHP
=CHANGELOG=
2017-12-10: 3.1.0
- Feature: Statistics page
- Enhancement: Upgrade corner menu image to support high DPI
- Enhancement: Mobile-friendly buttons in search results
- Enhancement: Explain keyboard shortcuts when hovering over form elements
- Enhancement: Improve performance when changing sources and when searching, by removing a network roundtrip
- Enhancement: "Latest" link takes you to the latest for the current data source
- Change ctrl+alt+[H/J/K/L/M]... keyboard shortcuts to just alt+[...], remove dependency on APL `mousetrap.js`
- Fix an issue with accessing the legacy php controller after the Go port
- Fix an issue with using plus characters in regular expression searches
- Fix an issue with blank pages appearing if there was a divisible number of log entries
- Fix an issue with browsers using stale stylesheets
- Fix a cosmetic issue with text selection when using dropdown menu items
- Fix a cosmetic issue with PCRE text in regular expression hover message (it's no longer PCRE)
- Fix a cosmetic issue with search box layout at some screen sizes
- Fix a cosmetic issue with animation responsiveness
2017-09-06: 3.0.1
- Breaking: Revert date formatting in filenames back to strftime-compatible
2017-08-13: 3.0.0
- Rewritten in Go.
- Enhancement: Standalone binary server

View File

@@ -25,9 +25,9 @@
/*
* The full path to the log file.
* Date formatting reference: https://golang.org/pkg/time/#pkg-constants
* Date formatting reference: http://strftime.org/
*/
"LogFilePath": "/path/to/my-log-source/2006-01.log",
"LogFilePath": "/path/to/my-log-source/%Y-%m.log",
/*
* The first month for which this path is valid.

View File

@@ -1,27 +1,9 @@
/* mousetrap v1.4.6 craig.is/killing/mice */
(function(J,r,f){function s(a,b,d){a.addEventListener?a.addEventListener(b,d,!1):a.attachEvent("on"+b,d)}function A(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return h[a.which]?h[a.which]:B[a.which]?B[a.which]:String.fromCharCode(a.which).toLowerCase()}function t(a){a=a||{};var b=!1,d;for(d in n)a[d]?b=!0:n[d]=0;b||(u=!1)}function C(a,b,d,c,e,v){var g,k,f=[],h=d.type;if(!l[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(g=0;g<l[a].length;++g)if(k=
l[a][g],!(!c&&k.seq&&n[k.seq]!=k.level||h!=k.action||("keypress"!=h||d.metaKey||d.ctrlKey)&&b.sort().join(",")!==k.modifiers.sort().join(","))){var m=c&&k.seq==c&&k.level==v;(!c&&k.combo==e||m)&&l[a].splice(g,1);f.push(k)}return f}function K(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function x(a,b,d,c){m.stopCallback(b,b.target||b.srcElement,d,c)||!1!==a(b,d)||(b.preventDefault?b.preventDefault():b.returnValue=!1,b.stopPropagation?
b.stopPropagation():b.cancelBubble=!0)}function y(a){"number"!==typeof a.which&&(a.which=a.keyCode);var b=A(a);b&&("keyup"==a.type&&z===b?z=!1:m.handleKey(b,K(a),a))}function w(a){return"shift"==a||"ctrl"==a||"alt"==a||"meta"==a}function L(a,b,d,c){function e(b){return function(){u=b;++n[a];clearTimeout(D);D=setTimeout(t,1E3)}}function v(b){x(d,b,a);"keyup"!==c&&(z=A(b));setTimeout(t,10)}for(var g=n[a]=0;g<b.length;++g){var f=g+1===b.length?v:e(c||E(b[g+1]).action);F(b[g],f,c,a,g)}}function E(a,b){var d,
c,e,f=[];d="+"===a?["+"]:a.split("+");for(e=0;e<d.length;++e)c=d[e],G[c]&&(c=G[c]),b&&"keypress"!=b&&H[c]&&(c=H[c],f.push("shift")),w(c)&&f.push(c);d=c;e=b;if(!e){if(!p){p={};for(var g in h)95<g&&112>g||h.hasOwnProperty(g)&&(p[h[g]]=g)}e=p[d]?"keydown":"keypress"}"keypress"==e&&f.length&&(e="keydown");return{key:c,modifiers:f,action:e}}function F(a,b,d,c,e){q[a+":"+d]=b;a=a.replace(/\s+/g," ");var f=a.split(" ");1<f.length?L(a,f,b,d):(d=E(a,d),l[d.key]=l[d.key]||[],C(d.key,d.modifiers,{type:d.action},
c,a,e),l[d.key][c?"unshift":"push"]({callback:b,modifiers:d.modifiers,action:d.action,seq:c,level:e,combo:a}))}var h={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},B={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},H={"~":"`","!":"1",
"@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},G={option:"alt",command:"meta","return":"enter",escape:"esc",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p,l={},q={},n={},D,z=!1,I=!1,u=!1;for(f=1;20>f;++f)h[111+f]="f"+f;for(f=0;9>=f;++f)h[f+96]=f;s(r,"keypress",y);s(r,"keydown",y);s(r,"keyup",y);var m={bind:function(a,b,d){a=a instanceof Array?a:[a];for(var c=0;c<a.length;++c)F(a[c],b,d);return this},
unbind:function(a,b){return m.bind(a,function(){},b)},trigger:function(a,b){if(q[a+":"+b])q[a+":"+b]({},a);return this},reset:function(){l={};q={};return this},stopCallback:function(a,b){return-1<(" "+b.className+" ").indexOf(" mousetrap ")?!1:"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable},handleKey:function(a,b,d){var c=C(a,b,d),e;b={};var f=0,g=!1;for(e=0;e<c.length;++e)c[e].seq&&(f=Math.max(f,c[e].level));for(e=0;e<c.length;++e)c[e].seq?c[e].level==f&&(g=!0,
b[c[e].seq]=1,x(c[e].callback,d,c[e].combo,c[e].seq)):g||x(c[e].callback,d,c[e].combo);c="keypress"==d.type&&I;d.type!=u||w(a)||c||t(b);I=g&&"keydown"==d.type}};J.Mousetrap=m;"function"===typeof define&&define.amd&&define(m)})(window,document);
/* archive.js */
function i(s) {
return document.getElementById(s);
}
var alreadyLoaded = false;
function t(e) {
e.style.display = (e.style.display == 'none') ? 'block' : 'none';
}
function urldesc(s) {
return decodeURIComponent(s.replace(/\+/g, " "));
}
var $chatArea = document.getElementById("chatarea");
var DEFAULT_FONT_SIZE = 12;
function cookie_set(key, value) {
document.cookie = (key+"="+value+"; expires=Sat, 20 Sep 2059 09:05:12; path=/");
@@ -42,6 +24,10 @@ function cookie_get(key) {
}
function highlight(str) {
var urldesc = function(s) {
return decodeURIComponent(s.replace(/\+/g, " "));
};
return str
.replace(/(\[\d\d\d\d-\d\d-\d\d\s\d\d\:\d\d\:\d\d\] )(\*.+)/g, "$1<span class=\"sys\">$2</span>")
.replace(
@@ -62,7 +48,7 @@ function highlight(str) {
function fontSize(change) {
var curSize = cookie_get("fontsize");
if (curSize === null) {
curSize = 12;
curSize = DEFAULT_FONT_SIZE;
} else {
curSize = + curSize;
}
@@ -71,25 +57,22 @@ function fontSize(change) {
cookie_set("fontsize", curSize);
i("chatarea").style["fontSize"] = ""+curSize+"px";
$chatArea.style["fontSize"] = ""+curSize+"px";
}
function toggleMenu() {
t(i("tr1"));
t(i("tr2"));
t(i("spm"));
var $container = document.getElementById("menu-container");
$container.style.display = ($container.style.display == 'block') ? 'none' : 'block';
}
function highlightLine(no) {
var lines = i('chatarea').innerHTML.split('<br>');
var lines = $chatArea.innerHTML.split("\n");
lines[no] = '<span class="line-highlighted">' + lines[no] + '</span>';
i('chatarea').innerHTML = lines.join('<br>');
$chatArea.innerHTML = lines.join("\n");
}
var alreadyLoaded = false;
function onLoad() {
if (alreadyLoaded) {
@@ -99,7 +82,11 @@ function onLoad() {
//
i('chatarea').innerHTML = highlight(i('chatarea').innerHTML);
$chatArea.innerHTML = highlight( $chatArea.innerHTML );
//
document.getElementById("logo").addEventListener("click", toggleMenu);
//
@@ -108,7 +95,6 @@ function onLoad() {
document.location.hash.substr(0, 6) === '#line-'
) {
highlightLine( parseInt(document.location.hash.substr(6), 10) );
document.location.hash = '';
}
//
@@ -117,60 +103,23 @@ function onLoad() {
//
i('selHub').onchange = function() {
if ( /\/search\//.test(window.location.pathname) ) {
window.location.pathname = i('selHub').value + "/search/" + encodeURIComponent( i('searchbox').value );
} else if ( /\/rx\//.test(window.location.pathname) ) {
window.location.pathname = i('selHub').value + "/rx/" + encodeURIComponent( i('searchbox').value )
} else {
i('frmHub').submit();
var $form = document.getElementById("search-form");
$form.addEventListener("submit", function(ev) {
var query = $form.elements["q"].value;
if (query.length === 0) {
alert("No search text entered");
ev.preventDefault();
return false;
}
};
//
var prefix = ($form.elements["rx"].checked ? "/rx/" : "/search/");
window.location.hash = "";
window.location.pathname = "/" + $form.elements["h"].value + prefix + encodeURIComponent(query);
Mousetrap.bind('ctrl+alt+h', function() { i("pgprev").click(); });
Mousetrap.bind('ctrl+alt+l', function() { i("pgnext").click(); });
Mousetrap.bind('ctrl+alt+j', function() {
YMgoto( YMmod(i("seldate").value, 1) )
});
Mousetrap.bind('ctrl+alt+k', function() {
YMgoto( YMmod(i("seldate").value, -1) )
ev.preventDefault();
return false;
});
Mousetrap.bind('ctrl+alt+m', function() {
i("searchbox").focus();
});
//
}
function YMmod(str, change) {
var t = str.split('-').map(function(x) { return +x; });
t[1] += change;
if (t[1] == 13) {
t[0] += 1;
t[1] = 1;
}
if (t[1] == 0) {
t[0] -= 1;
t[1] = 12;
}
return t.join('-');
}
function YMgoto(str) {
var t = str.split("-");
i("f_y").value = t[0];
i("f_m").value = t[1];
i("seldate").form.submit();
}
function setYM(el) {
YMgoto(el.value);
}
window.addEventListener('load', onLoad);

View File

@@ -1,16 +0,0 @@
<?php
/*
Chat Archives
`````````````
Requires PHP 5.4 (short-array syntax and ENT_SUBSTITUTE) with short_open_tag
URL rewriting for nginx;
location / {
try_files $uri /index.php?$args;
}
*/
require __DIR__.'/../includes/bootstrap.php';
URLRouter::routeRequest();

View File

@@ -61,6 +61,8 @@ select {
#chatarea {
word-break:break-word;
white-space: pre; /* Safari 1/2, IE 6/7 */
white-space: pre-wrap; /* Chrome, Firefox, IE8++ */
}
.timestamp {
@@ -77,14 +79,6 @@ select {
color: darkgreen;
}
.logo {
width: 102px;
height: 37px;
display: block;
border:0;
padding-bottom: 1.0em;
}
.gt {
color:#0A0;
font-weight:bold;
@@ -104,11 +98,18 @@ select {
.nav {
background: #DDD;
box-shadow: 0px 4px 24px #CCC;
font-size:0; /* remove phantom spaces between elements */
}
.nav form {
display: inline;
}
.nav a, .nav .current-page, .nav select {
font-size: 12px;
line-height: 12px;
}
.nav .btn {
background: white;
color:black;
@@ -117,8 +118,14 @@ select {
text-decoration:none;
}
.nav a {
transition:all 0.1s linear;
}
.nav a:hover {
border-color: grey black black grey;
transition:all 0s linear;
}
.area-search {
@@ -130,16 +137,33 @@ select {
}
#logo {
cursor:pointer;
display:inline-block;
width:16px;
height:16px;
background:transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAbtJREFUOMt9kb1qAlEQhacJ6QJBTJEiTSDpEqw1jYWIpQ9gIYIWghamELGysRE0Eo0GRRQrCyslIKIi/hUawVIEfYW8wGTOjbssyZqFbznMOeeyO5cOhwOdotls+sB/mZNGvV73tdttBtAnD9jv96aUy+XNZDJhAH0qR7vd7g+VSuWuVCrxZrNRQGNmlqXtdkvFYtFeKBR8uVzuTWgIH/j01WqlgMbs6L0hiw66JAN7NpvlfD7P1WoV/8uyOB4OhzydThXQmMFDBll00KVMJvOYSqV4Pp/zbDbjfr//L8ggiw66tF6vKZlMvkejUe71emppnU7HFHjIIIsOurRcLhWJROI5HA7v8KmtVssUeMggq/VosVjoiGEPBoNcq9VMgYeMsUOyJJ1QKPQai8VYNqyurtvtKqAxg4eMsUPj8VjH6/V+xuNxbjQaamHpdFoBjRk8ZIwdGo1GOg6H40u7Lo/Hg/AEQGvXi4yxQ4PBQBGJRC5sNhv7/X52u93bQCAQ1TxozOAhg6zm/byIboVHi8XidLlcr6KdgucXTjnkxWq1Pol+QEd15TQdec4Ei3AlXAv3R26Os0vh3Nj5Bg36USLeWI8KAAAAAElFTkSuQmCC') no-repeat 0 0;
margin-left:2px;
margin-right:4px;
}
/* Utility class */
.noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome and Opera */
}
/* Dropdown */
#menu-container {
display:none; /* default */
}
.ddmenu {
display:block;
position:absolute;
@@ -165,8 +189,6 @@ select {
.ddmenu a:hover {
background:#FFF;
-moz-transition:all 0.1s linear;
-webkit-transition:all 0.1s linear;
}
#tr1 {
@@ -202,7 +224,7 @@ select {
text-align:center;
}
.nav, .nav select {
.nav a, .nav .current-page, .nav select {
font-size: 16px;
line-height:16px;
}
@@ -227,12 +249,29 @@ select {
}
.area-search {
display:block;
float:none;
margin-top: 4px;
}
#searchbox {
width:auto;
}
ul.search-results {
list-style-type: none;
padding-left: 2px;
}
.search-results li a:first-child {
/* convert mini link to touchable */
display:inline-block;
padding: 2px 6px;
margin: 2px;
border: 1px solid grey;
background: lightgrey;
}
}
@media (max-width: 400px) {

File diff suppressed because one or more lines are too long

View File

@@ -25,7 +25,7 @@ func (this *URLHelper) intval(sz string) int {
}
func (this *URLHelper) get(sz string) string {
return this.r.URL.Query().Get(sz)
return this.r.URL.Query().Get(sz) // n.b. automatically unescaped
}
func (this *URLHelper) hasGet(sz string) bool {

19
vendor/github.com/jehiah/go-strftime/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2012 Jehiah Czebotar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

4
vendor/github.com/jehiah/go-strftime/README.md generated vendored Normal file
View File

@@ -0,0 +1,4 @@
go-strftime
===========
go implementation of strftime

72
vendor/github.com/jehiah/go-strftime/strftime.go generated vendored Normal file
View File

@@ -0,0 +1,72 @@
// go implementation of strftime
package strftime
import (
"strings"
"time"
)
// taken from time/format.go
var conversion = map[rune]string{
/*stdLongMonth */ 'B': "January",
/*stdMonth */ 'b': "Jan",
// stdNumMonth */ 'm': "1",
/*stdZeroMonth */ 'm': "01",
/*stdLongWeekDay */ 'A': "Monday",
/*stdWeekDay */ 'a': "Mon",
// stdDay */ 'd': "2",
// stdUnderDay */ 'd': "_2",
/*stdZeroDay */ 'd': "02",
/*stdHour */ 'H': "15",
// stdHour12 */ 'I': "3",
/*stdZeroHour12 */ 'I': "03",
// stdMinute */ 'M': "4",
/*stdZeroMinute */ 'M': "04",
// stdSecond */ 'S': "5",
/*stdZeroSecond */ 'S': "05",
/*stdLongYear */ 'Y': "2006",
/*stdYear */ 'y': "06",
/*stdPM */ 'p': "PM",
// stdpm */ 'p': "pm",
/*stdTZ */ 'Z': "MST",
// stdISO8601TZ */ 'z': "Z0700", // prints Z for UTC
// stdISO8601ColonTZ */ 'z': "Z07:00", // prints Z for UTC
/*stdNumTZ */ 'z': "-0700", // always numeric
// stdNumShortTZ */ 'b': "-07", // always numeric
// stdNumColonTZ */ 'b': "-07:00", // always numeric
/* nonStdMilli */ 'L': ".000",
}
// This is an alternative to time.Format because no one knows
// what date 040305 is supposed to create when used as a 'layout' string
// this takes standard strftime format options. For a complete list
// of format options see http://strftime.org/
func Format(format string, t time.Time) string {
retval := make([]byte, 0, len(format))
for i, ni := 0, 0; i < len(format); i = ni + 2 {
ni = strings.IndexByte(format[i:], '%')
if ni < 0 {
ni = len(format)
} else {
ni += i
}
retval = append(retval, []byte(format[i:ni])...)
if ni+1 < len(format) {
c := format[ni+1]
if c == '%' {
retval = append(retval, '%')
} else {
if layoutCmd, ok := conversion[rune(c)]; ok {
retval = append(retval, []byte(t.Format(layoutCmd))...)
} else {
retval = append(retval, '%', c)
}
}
} else {
if ni < len(format) {
retval = append(retval, '%')
}
}
}
return string(retval)
}

49
vendor/github.com/jehiah/go-strftime/strftime_test.go generated vendored Normal file
View File

@@ -0,0 +1,49 @@
package strftime
import (
"fmt"
"testing"
"time"
)
func ExampleFormat() {
t := time.Unix(1340244776, 0)
utc, _ := time.LoadLocation("UTC")
t = t.In(utc)
fmt.Println(Format("%Y-%m-%d %H:%M:%S", t))
// Output:
// 2012-06-21 02:12:56
}
func TestNoLeadingPercentSign(t *testing.T) {
tm := time.Unix(1340244776, 0)
utc, _ := time.LoadLocation("UTC")
tm = tm.In(utc)
result := Format("aaabbb0123456789%Y", tm)
if result != "aaabbb01234567892012" {
t.Logf("%s != %s", result, "aaabbb01234567892012")
t.Fail()
}
}
func TestUnsupported(t *testing.T) {
tm := time.Unix(1340244776, 0)
utc, _ := time.LoadLocation("UTC")
tm = tm.In(utc)
result := Format("%0%1%%%2", tm)
if result != "%0%1%%2" {
t.Logf("%s != %s", result, "%0%1%%2")
t.Fail()
}
}
func TestRubyStrftime(t *testing.T) {
tm := time.Unix(1340244776, 0)
utc, _ := time.LoadLocation("UTC")
tm = tm.In(utc)
result := Format("%H:%M:%S%L", tm)
if result != "02:12:56.000" {
t.Logf("%s != %s", result, "02:12:56.000")
t.Fail()
}
}