13 Commits

Author SHA1 Message Date
Alex Schroeder
e231412bdb Add list command 2023-12-09 09:21:24 +01:00
Alex Schroeder
0481d9003c Do not overwrite fresh backups
If the backup file is less than an hour old, do not overwrite it.
2023-11-24 13:49:41 +01:00
Alex Schroeder
7060f8a027 Fix handling of directories
For subdirectories, the redirect my be to an absolute URL starting
with /view.
2023-11-19 12:36:25 +01:00
Alex Schroeder
1cc5fcb823 Show index page for directory URLs 2023-11-19 12:21:51 +01:00
Alex Schroeder
f392d18dc9 Ensure empty line when adding 2023-11-18 23:48:13 +01:00
Alex Schroeder
6ab51afa30 Document the fact that HTML is not sanitized 2023-11-14 15:34:29 +01:00
Alex Schroeder
a73328ca2e Remove all HTML sanitization *again*!!
The previous revision is the one with HTML and SVG sanitization.
I'm removing it because I think the authors are all trusted users
and so they produce trusted HTML.
2023-11-14 14:07:32 +01:00
Alex Schroeder
ee3afc3384 Follow dependabot recommendations
Upgraded markdown, net, and image libraries.
2023-11-12 12:51:25 +01:00
Alex Schroeder
160ebd71e2 go fmt 2023-11-12 12:39:48 +01:00
Alex Schroeder
8900725737 go mod tidy 2023-11-12 12:38:55 +01:00
Alex Schroeder
f58476bba5 Merge branch 'svg-sanitization' 2023-11-12 12:22:45 +01:00
Alex Schroeder
dcb0cc7f51 Revert "No longer sanitize the HTML output!"
This reverts commit eb44880e8e.
2023-11-12 12:12:19 +01:00
Alex Schroeder
eb44880e8e No longer sanitize the HTML output! 2023-11-09 07:23:51 +01:00
27 changed files with 305 additions and 203 deletions

View File

@@ -10,7 +10,7 @@ import (
// initAccounts()
// p := &Page{Body: []byte(`@alex, @alex@alexschroeder.ch said`)}
// p.renderHtml()
// r := `<p>@alex, <a href="https://alexschroeder.ch/users/alex" rel="nofollow">@alex</a> said</p>
// r := `<p>@alex, <a href="https://alexschroeder.ch/users/alex">@alex</a> said</p>
// `
// assert.Equal(t, r, string(p.Html))
// }

View File

@@ -1,6 +1,7 @@
package main
import (
"bytes"
"log"
"net/http"
)
@@ -26,7 +27,7 @@ func appendHandler(w http.ResponseWriter, r *http.Request, name string) {
if err != nil {
p = &Page{Name: name, Body: []byte(body)}
} else {
p.Body = append(p.Body, []byte(body)...)
p.append([]byte(body))
}
p.handleTitle(false)
err = p.save()
@@ -42,3 +43,14 @@ func appendHandler(w http.ResponseWriter, r *http.Request, name string) {
}
http.Redirect(w, r, "/view/"+name, http.StatusFound)
}
func (p *Page) append(body []byte) {
// ensure an empty line at the end
if bytes.HasSuffix(p.Body, []byte("\n\n")) {
} else if bytes.HasSuffix(p.Body, []byte("\n")) {
p.Body = append(p.Body, '\n')
} else {
p.Body = append(p.Body, '\n', '\n')
}
p.Body = append(p.Body, body...)
}

View File

@@ -9,6 +9,18 @@ import (
"time"
)
func TestEmptyLineAdd(t *testing.T) {
p := &Page{Name: "testdata/add/fire", Body: []byte(`# Coal
Black rocks light as foam
Shaking, puring, shoveling`)}
p.append([]byte("Into the oven"))
assert.Equal(t, string(p.Body), `# Coal
Black rocks light as foam
Shaking, puring, shoveling
Into the oven`)
}
func TestAddAppend(t *testing.T) {
cleanup(t, "testdata/add")
index.load()
@@ -30,7 +42,7 @@ It's not `)}
"GET", "/add/testdata/add/fire", nil))
HTTPRedirectTo(t, makeHandler(appendHandler, true),
"POST", "/append/testdata/add/fire", data, "/view/testdata/add/fire")
assert.Regexp(t, regexp.MustCompile("Its not barbecue"),
assert.Regexp(t, regexp.MustCompile(`not</p>\s*<p>barbecue`),
assert.HTTPBody(makeHandler(viewHandler, true),
"GET", "/view/testdata/add/fire", nil))
}
@@ -51,15 +63,15 @@ Blue and green and pebbles gray
data.Set("body", "Stand in cold water")
data.Add("notify", "on")
HTTPRedirectTo(t, makeHandler(appendHandler, true),
"POST", "/append/testdata/notification2/" + today + "-water",
data, "/view/testdata/notification2/" + today + "-water")
"POST", "/append/testdata/notification2/"+today+"-water",
data, "/view/testdata/notification2/"+today+"-water")
// The changes.md file was created
s, err := os.ReadFile("changes.md")
assert.NoError(t, err)
d := time.Now().Format(time.DateOnly)
assert.Equal(t, "# Changes\n\n## " + d + "\n* [Water](testdata/notification2/" + today + "-water)\n", string(s))
assert.Equal(t, "# Changes\n\n## "+d+"\n* [Water](testdata/notification2/"+today+"-water)\n", string(s))
// Link added to index.md file
s, err = os.ReadFile("index.md")
assert.NoError(t, err)
assert.Contains(t, string(s), "\n* [Water](testdata/notification2/" + today + "-water)\n")
assert.Contains(t, string(s), "\n* [Water](testdata/notification2/"+today+"-water)\n")
}

View File

@@ -68,7 +68,7 @@ func addLinkWithDate(name, link string, re *regexp.Regexp) error {
// remove the preceding date if there are now two dates following each other
re := regexp.MustCompile(`(?m)^## (\d\d\d\d-\d\d-\d\d)\n\n## (\d\d\d\d-\d\d-\d\d)\n`)
if re.Match(c.Body[loc[0]-14 : loc[0]+15]) {
c.Body = append(c.Body[0 : loc[0]-14], c.Body[loc[0]+1 : ]...)
c.Body = append(c.Body[0:loc[0]-14], c.Body[loc[0]+1:]...)
}
} else if len(c.Body) == loc[0] {
// remove a trailing date
@@ -94,7 +94,7 @@ func addLinkWithDate(name, link string, re *regexp.Regexp) error {
m := re.Find(c.Body[loc[0]-14 : loc[0]])
if m == nil {
// not a date: insert date, don't move insertion point
} else if string(c.Body[loc[0]-11 : loc[0]-1]) == date {
} else if string(c.Body[loc[0]-11:loc[0]-1]) == date {
// if the date is our date, don't add it, don't move insertion point
addDate = false
} else {

View File

@@ -23,11 +23,11 @@ Out of sight and dark`)}
// Link added to changes.md file
s, err := os.ReadFile("changes.md")
assert.NoError(t, err)
assert.Contains(t, string(s), "[Washing machine](testdata/" + today + "-machine)")
assert.Contains(t, string(s), "[Washing machine](testdata/"+today+"-machine)")
// Link added to index.md file
s, err = os.ReadFile("index.md")
assert.NoError(t, err)
assert.Contains(t, string(s), "\n* [Washing machine](testdata/" + today + "-machine)\n")
assert.Contains(t, string(s), "\n* [Washing machine](testdata/"+today+"-machine)\n")
}
func TestChangesWithHashtag(t *testing.T) {
@@ -51,7 +51,7 @@ Home away from home
assert.Contains(t, string(s), line)
s, err = os.ReadFile("testdata/changes/Haiku.md")
assert.NoError(t, err)
assert.Equal(t, intro + line, string(s))
assert.Equal(t, intro+line, string(s))
assert.NoFileExists(t, "testdata/changes/Poetry.md")
}

View File

@@ -2,7 +2,9 @@ package main
import (
"github.com/stretchr/testify/assert"
"os"
"testing"
"time"
)
func TestDiff(t *testing.T) {
@@ -50,3 +52,45 @@ Mispronouncing words`
assert.Contains(t, body, `<del>s</del>`)
assert.Contains(t, body, `<ins>ce</ins>`)
}
func TestDiffBackup(t *testing.T) {
cleanup(t, "testdata/backup")
s := `# Cold Rooms
I shiver at home
the monitor glares and moans
fear or cold, who knows?`
r := `# Cold Rooms
I shiver at home
the monitor glares and moans
I hate the machine!`
u := `# Cold Rooms
I shiver at home
the monitor glares and moans
my grey heart grows cold`
p := &Page{Name: "testdata/backup/cold", Body: []byte(s)}
p.save()
p = &Page{Name: "testdata/backup/cold", Body: []byte(r)}
p.save()
body := string(p.Diff())
// diff from s to r:
assert.Contains(t, body, `<del>fear or cold, who knows?</del>`)
assert.Contains(t, body, `<ins>I hate the machine!</ins>`)
p = &Page{Name: "testdata/backup/cold", Body: []byte(u)}
p.save()
body = string(p.Diff())
// diff from s to u since r was not 60 min or older
assert.Contains(t, body, `<del>fear or cold, who knows?</del>`)
assert.Contains(t, body, `<ins>my grey heart grows cold</ins>`)
// set timestamp 2h in the past
ts := time.Now().Add(-2 * time.Hour)
assert.NoError(t, os.Chtimes("testdata/backup/cold.md~", ts, ts))
p = &Page{Name: "testdata/backup/cold", Body: []byte(r)}
p.save()
body = string(p.Diff())
// diff from u to r:
assert.Contains(t, body, `<del>my grey heart grows cold</del>`)
assert.Contains(t, body, `<ins>I hate the machine!</ins>`)
}

View File

@@ -51,7 +51,7 @@ func TestEditSaveChanges(t *testing.T) {
s, err := os.ReadFile("changes.md")
assert.NoError(t, err)
d := time.Now().Format(time.DateOnly)
assert.Equal(t, "# Changes\n\n## " + d +
assert.Equal(t, "# Changes\n\n## "+d+
"\n* [testdata/notification/2023-10-28-alex](testdata/notification/2023-10-28-alex)\n",
string(s))
// Link added to index.md file

16
go.mod
View File

@@ -4,21 +4,22 @@ go 1.21.0
require (
github.com/anthonynsimon/bild v0.13.0
github.com/gomarkdown/markdown v0.0.0-20230912175223-14b07df9d538
github.com/charmbracelet/lipgloss v0.8.0
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386
github.com/google/subcommands v1.2.0
github.com/hexops/gotextdiff v1.0.3
github.com/microcosm-cc/bluemonday v1.0.25
github.com/pemistahl/lingua-go v1.4.0
github.com/sergi/go-diff v1.3.1
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20221106115401-f9659909a136
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/lipgloss v0.8.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
@@ -26,13 +27,10 @@ require (
github.com/muesli/termenv v0.15.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
golang.org/x/exp v0.0.0-20221106115401-f9659909a136 // indirect
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/image v0.10.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

45
go.sum
View File

@@ -17,8 +17,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/gomarkdown/markdown v0.0.0-20230912175223-14b07df9d538 h1:ePDpFu7l0QUV46/9A7icfL2wvIOzTJLCWh4RO2NECzE=
github.com/gomarkdown/markdown v0.0.0-20230912175223-14b07df9d538/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 h1:EcQR3gusLHN46TAD+G+EbaaqJArt5vHhNpXAa12PQf4=
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -77,18 +77,49 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20221106115401-f9659909a136 h1:Fq7F/w7MAa1KJ5bt2aJ62ihqp9HDcRuyILskkpIAurw=
golang.org/x/exp v0.0.0-20221106115401-f9659909a136/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9 h1:uc17S921SPw5F2gJo7slQ3aqvr2RwpL7eb3+DZncu3s=
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/image v0.10.0 h1:gXjUUtwtx5yOE0VKWq1CH4IJAClq4UGgUA3i+rpON9M=
golang.org/x/image v0.10.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=

View File

@@ -15,9 +15,9 @@ func TestHtmlCmd(t *testing.T) {
<p>Hello! 🙃</p>
<p>Check out the <a href="README" rel="nofollow">README</a>.</p>
<p>Check out the <a href="README">README</a>.</p>
<p>Or <a href="test" rel="nofollow">create a new page</a>.</p>
<p>Or <a href="test">create a new page</a>.</p>
`
assert.Equal(t, r, b.String())

40
list_cmd.go Normal file
View File

@@ -0,0 +1,40 @@
package main
import (
"context"
"flag"
"fmt"
"github.com/google/subcommands"
"io"
"os"
)
type listCmd struct {
}
func (cmd *listCmd) SetFlags(f *flag.FlagSet) {
}
func (*listCmd) Name() string { return "list" }
func (*listCmd) Synopsis() string { return "List pages with name and title." }
func (*listCmd) Usage() string {
return `list:
List all pages with name and title, separated by a tabulator.
`
}
func (cmd *listCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
return listCli(os.Stdout, f.Args())
}
// listCli runs the list command on the command line. It is used
// here with an io.Writer for easy testing.
func listCli(w io.Writer, args []string) subcommands.ExitStatus {
index.load()
index.RLock()
defer index.RUnlock()
for name, title := range index.titles {
fmt.Fprintf(w, "%s\t%s\n", name, title)
}
return subcommands.ExitSuccess
}

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-REPLACE" "1" "2023-10-09"
.TH "ODDMU-REPLACE" "1" "2023-11-24"
.PP
.SH NAME
.PP
@@ -70,6 +70,32 @@ instead of making any changes and the regexp rules differ slightly.\&
The search is case-sensitive.\& To make it case-insensitive, search for a regular
expression that sets the case-insensitive flag, e.\&g.\& "(?\&i)oddmu".\&
.PP
.SH SECURITY
.PP
Consider creating a backup before doing replacements!\&
.PP
The following Bash script creates a copy of the current directory using hard
links.\& If you'\&re in a directory called "wiki", it creates a sibling directory
called "wiki-2023-11-24" (using the current date) full of links.\& This takes
little space and time.\& It works as a backup as long as you don'\&t use an
application that edits files in place.\& Most programs overwrite old files by
creating new files with the same name, so you should be safe.\&
.PP
.nf
.RS 4
#!/usr/bin/bash
d=$(basename $(pwd))
t=$(date --iso-8601)
echo Creating a snapshot of $d in \&.\&./$d-$t
rsync --link-dest "\&.\&./$d" --archive \&. "\&.\&./$d-$t/"
.fi
.RE
.PP
The above wouldn'\&t work for database files, for example.\& There, the database
changes the file in place thus the file is changed in the backup directory as
well.\& For Oddmu and the usual text editors, it works.\& If you use Emacs, don'\&t
set \fIbackup-by-copying\fR, \fIbackup-by-copying-when-linked\fR and related variables.\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-search\fR(7)

View File

@@ -55,6 +55,30 @@ instead of making any changes and the regexp rules differ slightly.
The search is case-sensitive. To make it case-insensitive, search for a regular
expression that sets the case-insensitive flag, e.g. "(?i)oddmu".
# SECURITY
Consider creating a backup before doing replacements!
The following Bash script creates a copy of the current directory using hard
links. If you're in a directory called "wiki", it creates a sibling directory
called "wiki-2023-11-24" (using the current date) full of links. This takes
little space and time. It works as a backup as long as you don't use an
application that edits files in place. Most programs overwrite old files by
creating new files with the same name, so you should be safe.
```
#!/usr/bin/bash
d=$(basename $(pwd))
t=$(date --iso-8601)
echo Creating a snapshot of $d in ../$d-$t
rsync --link-dest "../$d" --archive . "../$d-$t/"
```
The above wouldn't work for database files, for example. There, the database
changes the file in place thus the file is changed in the backup directory as
well. For Oddmu and the usual text editors, it works. If you use Emacs, don't
set _backup-by-copying_, _backup-by-copying-when-linked_ and related variables.
# SEE ALSO
_oddmu_(1), _oddmu-search_(7)

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU" "1" "2023-11-06"
.TH "ODDMU" "1" "2023-11-24"
.PP
.SH NAME
.PP
@@ -75,6 +75,10 @@ server as a reverse proxy.\&
.PP
See \fIoddmu-apache\fR(5) for an example.\&
.PP
Oddmu assumes that all the users that can edit pages or upload files are trusted
users and therefore their content is trusted.\& Therefore, Oddmu doesn'\&t perform
HTML sanitization!\&
.PP
.SH OPTIONS
.PP
The oddmu program can be run on the command-line using various subcommands.\&
@@ -131,7 +135,10 @@ Page names are filenames with ".\&md" appended.\& If your filesystem cannot hand
it, it can'\&t be a page name.\& Filenames can contain slashes and oddmu creates
subdirectories as necessary.\&
.PP
Files may not end with a tilde ('\&~'\&) these are backup files.\&
Files may not end with a tilde ('\&~'\&) these are backup files.\& When saving pages
and file uploads, the old file renamed to the backup file unless the backup file
is less than an hour old, thus collapsing all edits made in an hour into a
single diff when comparing backup and current version.\&
.PP
The \fBindex\fR page is the default page.\& People visiting the "root" of the site are
redirected to "/view/index".\&

View File

@@ -68,6 +68,10 @@ server as a reverse proxy.
See _oddmu-apache_(5) for an example.
Oddmu assumes that all the users that can edit pages or upload files are trusted
users and therefore their content is trusted. Therefore, Oddmu doesn't perform
HTML sanitization!
# OPTIONS
The oddmu program can be run on the command-line using various subcommands.
@@ -114,7 +118,10 @@ Page names are filenames with ".md" appended. If your filesystem cannot handle
it, it can't be a page name. Filenames can contain slashes and oddmu creates
subdirectories as necessary.
Files may not end with a tilde ('~') these are backup files.
Files may not end with a tilde ('~') these are backup files. When saving pages
and file uploads, the old file renamed to the backup file unless the backup file
is less than an hour old, thus collapsing all edits made in an hour into a
single diff when comparing backup and current version.
The *index* page is the default page. People visiting the "root" of the site are
redirected to "/view/index".

144
page.go
View File

@@ -40,131 +40,11 @@ func sanitizeStrict(s string) template.HTML {
return template.HTML(policy.Sanitize(s))
}
// santizeBytes uses bluemonday to sanitize the HTML used for pages. This is where you make changes if you want to be
// more lenient.
func sanitizeBytes(bytes []byte) template.HTML {
policy := bluemonday.UGCPolicy()
policy.AllowURLSchemes("gemini", "gopher")
policy.AllowAttrs("title", "class", "style").Globally()
policy.AllowAttrs("loading").OnElements("img") // for lazy loading
// SVG, based on https://svgwg.org/svg2-draft/attindex.html transformed using
// (while (zerop (forward-line 1))
// (when (looking-at "\\([^\t]+\\)\t\\([^\t]+\\).*")
// (let ((attribute (match-string 1))
// (elements (split-string (match-string 2) ", ")))
// (delete-region (point) (line-end-position))
// (insert "policy.AllowAttrs(\"" attribute "\").OnElements("
// (mapconcat (lambda (elem) (concat "\"" elem "\"")) elements ", ")
// ")"))))
// Manually delete "script", "crossorigin", all attributes starting with "on", "ping"
// and add elements without attributes allowed
// (while (re-search-forward "\tpolicy.AllowAttrs(\\(.*\\)).OnElements(\\(.*\\))\n\tpolicy.AllowAttrs(\\1).OnElements(\\(.*\\))" nil t)
// (replace-match "\tpolicy.AllowAttrs(\\1).OnElements(\\2, \\3)"))
// (while (re-search-forward "\tpolicy.AllowAttrs(\\(.*\\)).OnElements(\\(.*\\))\n\tpolicy.AllowAttrs(\\(.*\\)).OnElements(\\2)" nil t)
// (replace-match "\tpolicy.AllowAttrs(\\1, \\2).OnElements(\\3)"))
policy.AllowNoAttrs().OnElements("defs")
policy.AllowAttrs("alignment-baseline", "baseline-shift", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "cursor", "direction", "display", "dominant-baseline", "fill-opacity", "fill-rule", "filter", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "glyph-orientation-horizontal", "glyph-orientation-vertical", "image-rendering", "letter-spacing", "lighting-color", "marker-end", "marker-mid", "marker-start", "mask", "mask-type", "opacity", "overflow", "paint-order", "pointer-events", "shape-rendering", "stop-color", "stop-opacity", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-anchor", "text-decoration", "text-overflow", "text-rendering", "transform-origin", "unicode-bidi", "vector-effect", "visibility", "white-space", "word-spacing", "writing-mode").Globally() // SVG elements
policy.AllowAttrs("accumulate", "additive", "by", "calcMode", "from", "keySplines", "keyTimes", "values").OnElements("animate", "animateMotion", "animateTransform")
policy.AllowAttrs("amplitude").OnElements("feFuncA", "feFuncB", "feFuncG", "feFuncR")
policy.AllowAttrs("aria-activedescendant", "aria-atomic", "aria-autocomplete", "aria-busy", "aria-checked", "aria-colcount", "aria-colindex", "aria-colspan", "aria-controls", "aria-current", "aria-describedby", "aria-details", "aria-disabled", "aria-dropeffect", "aria-errormessage", "aria-expanded", "aria-flowto", "aria-grabbed", "aria-haspopup", "aria-hidden", "aria-invalid", "aria-keyshortcuts", "aria-label", "aria-labelledby", "aria-level", "aria-live", "aria-modal", "aria-multiline", "aria-multiselectable", "aria-orientation", "aria-owns", "aria-placeholder", "aria-posinset", "aria-pressed", "aria-readonly", "aria-relevant", "aria-required", "aria-roledescription", "aria-rowcount", "aria-rowindex", "aria-rowspan", "aria-selected", "aria-setsize", "aria-sort", "aria-valuemax", "aria-valuemin", "aria-valuenow", "aria-valuetext", "role").OnElements("a", "circle", "discard", "ellipse", "foreignObject", "g", "image", "line", "path", "polygon", "polyline", "rect", "svg", "switch", "symbol", "text", "textPath", "tspan", "use", "view")
policy.AllowAttrs("attributeName").OnElements("animate", "animateTransform", "set")
policy.AllowAttrs("autofocus").OnElements("a", "animate", "animateMotion", "animateTransform", "circle", "clipPath", "defs", "desc", "discard", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "foreignObject", "g", "image", "line", "linearGradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "set", "stop", "style", "svg", "switch", "symbol", "text", "textPath", "title", "tspan", "use", "view")
policy.AllowAttrs("azimuth", "elevation").OnElements("feDistantLight")
policy.AllowAttrs("baseFrequency", "numOctaves", "seed", "stitchTiles").OnElements("feTurbulence")
policy.AllowAttrs("begin").OnElements("animate", "animateMotion", "animateTransform", "set", "discard")
policy.AllowAttrs("bias", "divisor", "kernelMatrix", "order", "preserveAlpha", "targetX", "targetY").OnElements("feConvolveMatrix")
policy.AllowAttrs("class").OnElements("a", "animate", "animateMotion", "animateTransform", "circle", "clipPath", "defs", "desc", "discard", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "foreignObject", "g", "image", "line", "linearGradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "set", "stop", "style", "svg", "switch", "symbol", "text", "textPath", "title", "tspan", "use", "view")
policy.AllowAttrs("clipPathUnits").OnElements("clipPath")
policy.AllowAttrs("cx", "cy").OnElements("circle", "ellipse", "radialGradient")
policy.AllowAttrs("d").OnElements("path")
policy.AllowAttrs("diffuseConstant").OnElements("feDiffuseLighting")
policy.AllowAttrs("download").OnElements("a")
policy.AllowAttrs("dur").OnElements("animate", "animateMotion", "animateTransform", "set")
policy.AllowAttrs("dx", "dy").OnElements("feDropShadow", "feOffset", "text", "tspan")
policy.AllowAttrs("edgeMode").OnElements("feConvolveMatrix", "feGaussianBlur")
policy.AllowAttrs("end").OnElements("animate", "animateMotion", "animateTransform", "set")
policy.AllowAttrs("exponent").OnElements("feFuncA", "feFuncB", "feFuncG", "feFuncR")
policy.AllowAttrs("fill").Globally() // at least for all SVG elements
policy.AllowAttrs("filterUnits").OnElements("filter")
policy.AllowAttrs("fr", "fx", "fy").OnElements("radialGradient")
policy.AllowAttrs("gradientTransform", "gradientUnits").OnElements("linearGradient", "radialGradient")
policy.AllowAttrs("height").OnElements("feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDropShadow", "feFlood", "feGaussianBlur", "feImage", "feMerge", "feMorphology", "feOffset", "feSpecularLighting", "feTile", "feTurbulence", "filter", "mask", "pattern", "foreignObject", "image", "rect", "svg", "symbol", "use")
policy.AllowAttrs("href").OnElements("a", "animate", "animateMotion", "animateTransform", "set", "discard", "feImage", "image", "linearGradient", "mpath", "pattern", "radialGradient", "textPath", "use")
policy.AllowAttrs("hreflang").OnElements("a")
policy.AllowAttrs("id").OnElements("a", "animate", "animateMotion", "animateTransform", "circle", "clipPath", "defs", "desc", "discard", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "foreignObject", "g", "image", "line", "linearGradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "set", "stop", "style", "svg", "switch", "symbol", "text", "textPath", "title", "tspan", "use", "view")
policy.AllowAttrs("in").OnElements("feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDropShadow", "feGaussianBlur", "feMergeNode", "feMorphology", "feOffset", "feSpecularLighting", "feTile")
policy.AllowAttrs("in2").OnElements("feBlend", "feComposite", "feDisplacementMap")
policy.AllowAttrs("intercept").OnElements("feFuncA", "feFuncB", "feFuncG", "feFuncR")
policy.AllowAttrs("k1", "k2", "k3", "k4").OnElements("feComposite")
policy.AllowAttrs("kernelUnitLength").OnElements("feConvolveMatrix", "feDiffuseLighting", "feSpecularLighting")
policy.AllowAttrs("keyPoints").OnElements("animateMotion")
policy.AllowAttrs("lang").OnElements("a", "animate", "animateMotion", "animateTransform", "circle", "clipPath", "defs", "desc", "discard", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "foreignObject", "g", "image", "line", "linearGradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "set", "stop", "style", "svg", "switch", "symbol", "text", "textPath", "title", "tspan", "use", "view")
policy.AllowAttrs("lengthAdjust").OnElements("text", "textPath", "tspan")
policy.AllowAttrs("limitingConeAngle").OnElements("feSpotLight")
policy.AllowAttrs("markerHeight", "markerUnits", "markerWidth").OnElements("marker")
policy.AllowAttrs("maskContentUnits", "mask").OnElements("maskUnits")
policy.AllowAttrs("max").OnElements("animate", "animateMotion", "animateTransform", "set")
policy.AllowAttrs("media").OnElements("style")
policy.AllowAttrs("method").OnElements("textPath")
policy.AllowAttrs("min").OnElements("animate", "animateMotion", "animateTransform", "set")
policy.AllowAttrs("mode").OnElements("feBlend")
policy.AllowAttrs("offset").OnElements("feFuncA", "feFuncB", "feFuncG", "feFuncR", "stop")
policy.AllowAttrs("operator").OnElements("feComposite", "feMorphology")
policy.AllowAttrs("orient").OnElements("marker")
policy.AllowAttrs("origin").OnElements("animateMotion")
policy.AllowAttrs("path").OnElements("animateMotion", "textPath")
policy.AllowAttrs("pathLength").OnElements("circle", "ellipse", "line", "path", "polygon", "polyline", "rect")
policy.AllowAttrs("patternContentUnits", "pattern").OnElements("patternTransform")
policy.AllowAttrs("patternUnits").OnElements("pattern")
policy.AllowAttrs("playbackorder", "timelinebegin", "transform").OnElements("svg")
policy.AllowAttrs("points").OnElements("polygon", "polyline")
policy.AllowAttrs("pointsAtX", "feSpotLight").OnElements("pointsAtY")
policy.AllowAttrs("pointsAtZ").OnElements("feSpotLight")
policy.AllowAttrs("preserveAspectRatio").OnElements("feImage", "image", "marker", "pattern", "svg", "symbol", "view")
policy.AllowAttrs("primitiveUnits").OnElements("filter")
policy.AllowAttrs("r").OnElements("circle", "radialGradient")
policy.AllowAttrs("rx", "ry").OnElements("ellipse", "rect")
policy.AllowAttrs("radius").OnElements("feMorphology")
policy.AllowAttrs("refX", "marker", "symbol").OnElements("refY")
policy.AllowAttrs("referrerpolicy", "a").OnElements("rel")
policy.AllowAttrs("repeatCount", "animate", "animateMotion", "animateTransform", "set").OnElements("repeatDur")
policy.AllowAttrs("requiredExtensions").OnElements("a", "animate", "animateMotion", "animateTransform", "circle", "clipPath", "discard", "ellipse", "foreignObject", "g", "image", "line", "mask", "path", "polygon", "polyline", "rect", "set", "svg", "switch", "text", "textPath", "tspan", "use")
policy.AllowAttrs("restart").OnElements("animate", "animateMotion", "animateTransform", "set")
policy.AllowAttrs("result").OnElements("feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDropShadow", "feFlood", "feGaussianBlur", "feImage", "feMerge", "feMorphology", "feOffset", "feSpecularLighting", "feTile", "feTurbulence")
policy.AllowAttrs("rotate").OnElements("animateMotion", "text", "tspan")
policy.AllowAttrs("scale").OnElements("feDisplacementMap")
policy.AllowAttrs("side").OnElements("textPath")
policy.AllowAttrs("slope").OnElements("feFuncA", "feFuncB", "feFuncG", "feFuncR")
policy.AllowAttrs("spacing").OnElements("textPath")
policy.AllowAttrs("specularConstant").OnElements("feSpecularLighting")
policy.AllowAttrs("specularExponent").OnElements("feSpecularLighting", "feSpotLight")
policy.AllowAttrs("spreadMethod").OnElements("linearGradient", "radialGradient")
policy.AllowAttrs("startOffset").OnElements("textPath")
policy.AllowAttrs("stdDeviation").OnElements("feDropShadow", "feGaussianBlur")
policy.AllowAttrs("style").OnElements("a", "animate", "animateMotion", "animateTransform", "circle", "clipPath", "defs", "desc", "discard", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "foreignObject", "g", "image", "line", "linearGradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "set", "stop", "style", "svg", "switch", "symbol", "text", "textPath", "title", "tspan", "use", "view")
policy.AllowAttrs("surfaceScale").OnElements("feDiffuseLighting", "feSpecularLighting")
policy.AllowAttrs("systemLanguage").OnElements("a", "animate", "animateMotion", "animateTransform", "circle", "clipPath", "discard", "ellipse", "foreignObject", "g", "image", "line", "mask", "path", "polygon", "polyline", "rect", "set", "svg", "switch", "text", "textPath", "tspan", "use")
policy.AllowAttrs("tabindex").OnElements("a", "animate", "animateMotion", "animateTransform", "circle", "clipPath", "defs", "desc", "discard", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "foreignObject", "g", "image", "line", "linearGradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "set", "stop", "style", "svg", "switch", "symbol", "text", "textPath", "title", "tspan", "use", "view")
policy.AllowAttrs("tableValues").OnElements("feFuncA", "feFuncB", "feFuncG", "feFuncR")
policy.AllowAttrs("target").OnElements("a")
policy.AllowAttrs("textLength").OnElements("text", "textPath", "tspan")
policy.AllowAttrs("title").OnElements("style")
policy.AllowAttrs("to").OnElements("animate", "animateMotion", "animateTransform", "set")
policy.AllowAttrs("transform").Globally() // for almost all SVG elements (with the exception of the pattern, linearGradient and radialGradient elements)
policy.AllowAttrs("type").OnElements("a", "animateTransform", "feColorMatrix", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feTurbulence", "style")
policy.AllowAttrs("values").OnElements("feColorMatrix")
policy.AllowAttrs("viewBox").OnElements("marker", "pattern", "svg", "symbol", "view")
policy.AllowAttrs("width").OnElements("feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDropShadow", "feFlood", "feGaussianBlur", "feImage", "feMerge", "feMorphology", "feOffset", "feSpecularLighting", "feTile", "feTurbulence", "filter", "mask", "pattern", "foreignObject", "image", "rect", "svg", "symbol", "use")
policy.AllowAttrs("x").OnElements("feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDropShadow", "feFlood", "feGaussianBlur", "feImage", "feMerge", "feMorphology", "feOffset", "feSpecularLighting", "feTile", "feTurbulence", "fePointLight", "feSpotLight", "filter", "mask", "pattern", "text", "tspan", "foreignObject", "image", "rect", "svg", "symbol", "use")
policy.AllowAttrs("x1", "x2", "y1", "y2").OnElements("line", "linearGradient")
policy.AllowAttrs("xChannelSelector").OnElements("feDisplacementMap")
policy.AllowAttrs("xlink:href").OnElements("a", "image", "linearGradient", "pattern", "radialGradient", "textPath", "use", "feImage")
policy.AllowAttrs("xlink:title").OnElements("a", "image", "linearGradient", "pattern", "radialGradient", "textPath", "use")
policy.AllowAttrs("xml:space").OnElements("a", "animate", "animateMotion", "animateTransform", "circle", "clipPath", "defs", "desc", "discard", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "foreignObject", "g", "image", "line", "linearGradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "set", "stop", "style", "svg", "switch", "symbol", "text", "textPath", "title", "tspan", "use", "view")
policy.AllowAttrs("y").OnElements("feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDropShadow", "feFlood", "feGaussianBlur", "feImage", "feMerge", "feMorphology", "feOffset", "feSpecularLighting", "feTile", "feTurbulence", "fePointLight", "feSpotLight", "filter", "mask", "pattern", "text", "tspan", "foreignObject", "image", "rect", "svg", "symbol", "use")
policy.AllowAttrs("yChannelSelector").OnElements("feDisplacementMap")
policy.AllowAttrs("z").OnElements("fePointLight", "feSpotLight")
return template.HTML(policy.SanitizeBytes(bytes))
// unsafeBytes does not use bluemonday to sanitize the HTML used for pages. This is where you make changes if you want
// to be more lenient. If you look at the git repository, there are older versions containing the function sanitizeBytes
// which would do elaborate checking.
func unsafeBytes(bytes []byte) template.HTML {
return template.HTML(bytes)
}
// nameEscape returns the page name safe for use in URLs. That is,
@@ -200,10 +80,22 @@ func (p *Page) save() error {
return err
}
}
_ = os.Rename(filename, filename+"~")
backup(filename)
return os.WriteFile(filename, s, 0644)
}
// backup a file by renaming (!) it unless the existing backup is less than an hour old. A backup gets a tilde appended
// to it ("~"). This is true even if the file refers to a binary file like "image.png" and most applications don't know
// what to do with a file called "image.png~".
func backup(filename string) error {
backup := filename + "~"
fi, err := os.Stat(backup)
if err != nil || time.Since(fi.ModTime()).Minutes() >= 60 {
return os.Rename(filename, backup)
}
return nil
}
// loadPage loads a Page given a name. The filename loaded is that
// Page.Name with the ".md" extension. The Page.Title is set to the
// Page.Name (and possibly changed, later). The Page.Body is set to

View File

@@ -4,8 +4,8 @@ import (
"bytes"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/ast"
"github.com/gomarkdown/markdown/parser"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"net/url"
)
@@ -81,7 +81,7 @@ func wikiRenderer() *html.Renderer {
renderer := html.NewRenderer(opts)
return renderer
}
// renderHtml renders the Page.Body to HTML and sets Page.Html, Page.Language, Page.Hashtags, and escapes Page.Name.
// Note: If the rendered HTML doesn't contain the attributes or elements you expect it to contain, check sanitizeBytes!
func (p *Page) renderHtml() {
@@ -89,7 +89,7 @@ func (p *Page) renderHtml() {
renderer := wikiRenderer()
maybeUnsafeHTML := markdown.ToHTML(p.Body, parser, renderer)
p.Name = nameEscape(p.Name)
p.Html = sanitizeBytes(maybeUnsafeHTML)
p.Html = unsafeBytes(maybeUnsafeHTML)
p.Language = language(p.plainText())
p.Hashtags = *hashtags
}

View File

@@ -43,7 +43,7 @@ I am cold, alone
Too faint to focus, so far
I am cold, alone</p>
<p><a class="tag" href="/search/?q=%23Haiku" rel="nofollow">#Haiku</a> <a class="tag" href="/search/?q=%23Cold_Poets" rel="nofollow">#Cold Poets</a></p>
<p><a class="tag" href="/search/?q=%23Haiku">#Haiku</a> <a class="tag" href="/search/?q=%23Cold_Poets">#Cold Poets</a></p>
`
assert.Equal(t, r, string(p.Html))
}
@@ -57,8 +57,8 @@ Our [[time together]]`)}
r := `<h1>Photos and Books</h1>
<p>Blue and green and black
Sky and grass and <a href="cliffs" rel="nofollow">ragged cliffs</a>
Our <a href="time%20together" rel="nofollow">time together</a></p>
Sky and grass and <a href="cliffs">ragged cliffs</a>
Our <a href="time%20together">time together</a></p>
`
assert.Equal(t, r, string(p.Html))
}

View File

@@ -200,7 +200,7 @@ NameLoop:
// prependQueryPage prepends the query itself, if a matching page name exists. This helps if people remember the name
// exactly, or if searching for a hashtag. This function assumes that q is not the empty string. Return wether a page
// was prepended or not.
func prependQueryPage (names []string, dir, q string) ([]string, bool) {
func prependQueryPage(names []string, dir, q string) ([]string, bool) {
index.RLock()
defer index.RUnlock()
if q[0] == '#' && !strings.Contains(q[1:], "#") {

View File

@@ -13,8 +13,8 @@ import (
)
type searchCmd struct {
page int
all bool
page int
all bool
extract bool
}
@@ -46,16 +46,16 @@ func searchCli(w io.Writer, n int, all, extract bool, quiet bool, args []string)
q := strings.Join(args, " ")
items, more := search(q, ".", n, true)
if !quiet {
fmt.Fprint(os.Stderr, "Search for ", q)
if !all {
fmt.Fprint(os.Stderr, ", page ", n)
}
fmt.Fprint(os.Stderr, ": ", len(items))
if len(items) == 1 {
fmt.Fprint(os.Stderr, " result\n")
} else {
fmt.Fprint(os.Stderr, " results\n")
}
fmt.Fprint(os.Stderr, "Search for ", q)
if !all {
fmt.Fprint(os.Stderr, ", page ", n)
}
fmt.Fprint(os.Stderr, ": ", len(items))
if len(items) == 1 {
fmt.Fprint(os.Stderr, " result\n")
} else {
fmt.Fprint(os.Stderr, " results\n")
}
}
if extract {
searchExtract(w, items)

View File

@@ -29,14 +29,13 @@ func TestSortNames(t *testing.T) {
assert.True(t, slices.IsSorted(names), fmt.Sprintf("Sorted: %v", names))
}
func TestPrependMatches(t *testing.T) {
index.Lock()
for _, s := range []string{"Alex", "Berta", "Chris"} {
index.titles[s] = s
}
index.Unlock()
r := []string{"Berta", "Chris"} // does not prepend
r := []string{"Berta", "Chris"} // does not prepend
u := []string{"Alex", "Berta", "Chris"} // does prepend
v, _ := prependQueryPage(r, "", "Alex")
assert.Equal(t, u, v, "prepend q")
@@ -130,10 +129,10 @@ We met in the park?`)}
func TestHashtagSearch(t *testing.T) {
cleanup(t, "testdata/hashtag")
p := &Page{Name: "testdata/hashtag/Haiku", Body: []byte("# Haikus\n")}
p.save()
p = &Page{Name: "testdata/hashtag/2023-10-28", Body: []byte(`# Tea
My tongue is on fire

View File

@@ -100,7 +100,7 @@ func staticPage(filename, dir string) error {
renderer := html.NewRenderer(opts)
maybeUnsafeHTML := markdown.Render(doc, renderer)
p.Name = nameEscape(p.Name)
p.Html = sanitizeBytes(maybeUnsafeHTML)
p.Html = unsafeBytes(maybeUnsafeHTML)
p.Language = language(p.plainText())
p.Hashtags = *hashtags
return p.write(filepath.Join(dir, name+".html"))

View File

@@ -89,11 +89,7 @@ func dropHandler(w http.ResponseWriter, r *http.Request, dir string) {
return
}
defer file.Close()
// backup an existing file with the same name
_, err = os.Stat(filename)
if err != nil {
os.Rename(filename, filename+"~")
}
backup(filename)
// create the new file
path := d + "/" + filename
dst, err := os.Create(path)

27
view.go
View File

@@ -3,6 +3,7 @@ package main
import (
"net/http"
"os"
"path"
"strings"
"time"
)
@@ -12,19 +13,22 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/view/index", http.StatusFound)
}
// viewHandler serves existing files (including markdown files with
// the .md extension). If the requested file does not exist, a page
// with the same name is loaded. This means adding the .md extension
// and using the "view.html" template to render the HTML. Both
// attempts fail, the browser is redirected to an edit page. As far as
// caching goes: we respond with a 304 NOT MODIFIED if the request has
// an If-Modified-Since header that matches the file's modification
// time, truncated to one second, because the file's modtime has
// sub-second precision and the HTTP timestamp for the Last-Modified
// header has not.
// viewHandler serves pages. If the requested URL maps to an existing file, it is served. If the requested URL maps to a
// directory, the browser is redirected to the index page. If the requested URL ends in ".rss" and the corresponding
// file ending with ".md" exists, a feed is generated and the "feed.html" template is used (it is used to generate a RSS
// 2.0 feed, no matter what the template's extension is). If the requested URL maps to a page name, the corresponding
// file (ending in ".md") is loaded and served using the "view.html" template. If none of the above, the browser is
// redirected to an edit page.
//
// Caching: a 304 NOT MODIFIED is returned if the request has an If-Modified-Since header that matches the file's
// modification time, truncated to one second. Truncation is required because the file's modtime has sub-second
// precision and the HTTP timestamp for the Last-Modified header has not.
func viewHandler(w http.ResponseWriter, r *http.Request, name string) {
file := true
rss := false
if name == "" {
name = "."
}
fn := name
fi, err := os.Stat(fn)
if err != nil {
@@ -36,6 +40,9 @@ func viewHandler(w http.ResponseWriter, r *http.Request, name string) {
}
fn += ".md"
fi, err = os.Stat(fn)
} else if fi.IsDir() {
http.Redirect(w, r, path.Join("/view", name, "index"), http.StatusFound)
return
}
if err == nil {
h, ok := r.Header["If-Modified-Since"]

View File

@@ -3,10 +3,10 @@ package main
import (
"github.com/stretchr/testify/assert"
"net/http"
"net/url"
"os"
"regexp"
"testing"
"net/url"
)
func TestRootHandler(t *testing.T) {
@@ -19,6 +19,12 @@ func TestViewHandler(t *testing.T) {
assert.HTTPBody(makeHandler(viewHandler, true), "GET", "/view/index", nil))
}
func TestViewHandlerDir(t *testing.T) {
HTTPRedirectTo(t, makeHandler(viewHandler, true), "GET", "/view/", nil, "/view/index")
HTTPRedirectTo(t, makeHandler(viewHandler, true), "GET", "/view/man", nil, "/view/man/index")
HTTPRedirectTo(t, makeHandler(viewHandler, true), "GET", "/view/man/", nil, "/view/man/index")
}
// relies on index.md in the current directory!
func TestViewHandlerWithId(t *testing.T) {
data := make(url.Values)

View File

@@ -126,6 +126,7 @@ func commands() {
subcommands.Register(subcommands.FlagsCommand(), "")
subcommands.Register(subcommands.CommandsCommand(), "")
subcommands.Register(&htmlCmd{}, "")
subcommands.Register(&listCmd{}, "")
subcommands.Register(&staticCmd{}, "")
subcommands.Register(&searchCmd{}, "")
subcommands.Register(&replaceCmd{}, "")

View File

@@ -95,12 +95,12 @@ func restore(t *testing.T, files ...string) {
s, err := os.Stat(file)
if err != nil {
t.Log("Could not stat ", file, ": ", err)
continue;
continue
}
c, err := os.ReadFile(file)
if err != nil {
t.Log("Could not read ", file, ": ", err)
continue;
continue
}
stat[file] = s
data[file] = c