cleanup
This commit is contained in:
parent
10e0bde07b
commit
4540797cce
|
@ -1,26 +0,0 @@
|
|||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
users/
|
||||
|
||||
.vscode/
|
||||
*.swp
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
}
|
33
Dockerfile
33
Dockerfile
|
@ -1,33 +0,0 @@
|
|||
##
|
||||
## Build Container
|
||||
##
|
||||
FROM golang:1.19-alpine as build
|
||||
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
WORKDIR /tmp/owl
|
||||
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go build -o ./out/owl ./cmd/owl
|
||||
|
||||
|
||||
##
|
||||
## Run Container
|
||||
##
|
||||
FROM alpine:3.9
|
||||
RUN apk add ca-certificates
|
||||
|
||||
COPY --from=build /tmp/owl/out/ /bin/
|
||||
|
||||
# This container exposes port 8080 to the outside world
|
||||
EXPOSE 8080
|
||||
|
||||
# Run the binary program produced by `go install`
|
||||
ENTRYPOINT ["/bin/owl"]
|
BIN
assets/owl.png
BIN
assets/owl.png
Binary file not shown.
Before Width: | Height: | Size: 46 KiB |
200
assets/owl.svg
200
assets/owl.svg
|
@ -1,200 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="100mm"
|
||||
height="100mm"
|
||||
viewBox="0 0 100 100"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||
sodipodi:docname="owl.svg">
|
||||
<defs
|
||||
id="defs2">
|
||||
<inkscape:path-effect
|
||||
effect="mirror_symmetry"
|
||||
start_point="101.5,113.98198"
|
||||
end_point="101.5,177.55836"
|
||||
center_point="101.5,145.77017"
|
||||
id="path-effect4762"
|
||||
is_visible="true"
|
||||
mode="free"
|
||||
discard_orig_path="false"
|
||||
fuse_paths="true"
|
||||
oposite_fuse="false" />
|
||||
<inkscape:path-effect
|
||||
effect="mirror_symmetry"
|
||||
start_point="101.6,77.962793"
|
||||
end_point="101.6,178.13471"
|
||||
center_point="101.6,128.04875"
|
||||
id="path-effect4630"
|
||||
is_visible="true"
|
||||
mode="free"
|
||||
discard_orig_path="false"
|
||||
fuse_paths="true"
|
||||
oposite_fuse="false" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.98994949"
|
||||
inkscape:cx="-5.4384962"
|
||||
inkscape:cy="476.07169"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer5"
|
||||
showgrid="false"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1391"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Body"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
style="display:inline"
|
||||
transform="translate(0,-197)" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer6"
|
||||
inkscape:label="Front"
|
||||
transform="translate(0,-197)" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer5"
|
||||
inkscape:label="Ref"
|
||||
style="display:inline"
|
||||
transform="translate(0,-197)" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Feet"
|
||||
transform="translate(0,-197)" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Nose"
|
||||
transform="translate(0,-197)" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Eyes"
|
||||
transform="translate(0,-197)">
|
||||
<path
|
||||
style="display:inline;opacity:1;fill:#686560;fill-opacity:1;stroke:#000000;stroke-width:1.46500003;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 72.665922,98.468004 c -3.804657,-4.064063 -8.937651,-7.726186 -7.559523,-20.505208 1.511903,2.173362 6.898064,8.031993 12.095236,8.031993 7.291682,0 22.616673,0.08369 24.398365,0.09355 1.78169,-0.0099 17.10668,-0.09355 24.39836,-0.09355 5.19718,0 10.58334,-5.858631 12.09524,-8.031993 1.37813,12.779022 -3.75487,16.441145 -7.55952,20.505208 0,0 4.84169,14.668796 9.88119,20.229646 10.76577,11.87955 9.28012,38.96737 -13.7775,54.51257 -4.9083,3.30912 -14.73063,5.13452 -25.03777,4.90519 -10.307138,0.22933 -20.129467,-1.59607 -25.037771,-4.90519 -23.057616,-15.5452 -24.543272,-42.63302 -13.777496,-54.51257 5.039494,-5.56085 9.881189,-20.229646 9.881189,-20.229646 z"
|
||||
id="path46"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccsccssc"
|
||||
inkscape:path-effect="#path-effect4630"
|
||||
inkscape:original-d="m 72.665922,98.468004 c -3.804657,-4.064063 -8.937651,-7.726186 -7.559523,-20.505208 1.511903,2.173362 6.898064,8.031993 12.095236,8.031993 7.748513,0 24.568455,0.0945 24.568455,0.0945 9.17095,40.967981 -1.73329,86.382581 24.08866,86.819411 -14.73188,7.45015 -40.288984,6.3743 -49.296521,0.30152 -23.057616,-15.5452 -24.543272,-42.63302 -13.777496,-54.51257 5.039494,-5.56085 9.881189,-20.229646 9.881189,-20.229646 z"
|
||||
transform="matrix(0.89013051,0,0,0.89013051,-40.43726,131.70004)" />
|
||||
<path
|
||||
style="display:inline;fill:#fbe9c4;fill-opacity:1;stroke:#262626;stroke-width:4.06500006;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 74.568197,129.95654 c -1.207452,8.188 -1.187282,34.70878 17.572973,44.03265 3.623482,1.36045 5.80487,3.13501 9.35883,3.62684 3.55396,-0.49183 5.73535,-2.26639 9.35883,-3.62684 18.76025,-9.32387 18.78042,-35.84465 17.57297,-44.03265 C 126.73222,117.4929 107.35585,115.8851 101.5,115.68436 95.644152,115.8851 76.267784,117.4929 74.568197,129.95654 Z"
|
||||
id="path4760"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccscc"
|
||||
inkscape:path-effect="#path-effect4762"
|
||||
inkscape:original-d="m 74.568197,129.95654 c -1.207452,8.188 -1.187282,34.70878 17.572973,44.03265 4.717147,1.77107 6.990299,4.24396 13.02939,3.67496 5.2517,-4.28707 -4.51571,-38.74118 3.27405,-50.98166 11.76071,-18.48023 -5.27857,-11.02487 -5.27857,-11.02487 0,0 -26.593322,-0.4009 -28.597843,14.29892 z"
|
||||
transform="matrix(0.89013051,0,0,0.89013051,-40.43726,131.70004)" />
|
||||
<rect
|
||||
style="opacity:1;fill:#674808;fill-opacity:1;stroke:#efc48c;stroke-width:2.72825003;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4767"
|
||||
width="23.046696"
|
||||
height="2.0186887"
|
||||
x="39.805622"
|
||||
y="258.96619" />
|
||||
<rect
|
||||
style="opacity:1;fill:#674808;fill-opacity:1;stroke:#efc48c;stroke-width:2.72825003;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4767-7"
|
||||
width="23.046698"
|
||||
height="2.0186887"
|
||||
x="39.805622"
|
||||
y="266.51617" />
|
||||
<rect
|
||||
style="opacity:1;fill:#674808;fill-opacity:1;stroke:#efc48c;stroke-width:2.72825003;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4767-7-0"
|
||||
width="23.046698"
|
||||
height="2.0186887"
|
||||
x="39.80563"
|
||||
y="274.1886" />
|
||||
<path
|
||||
style="display:inline;opacity:1;fill:#f1cd6b;fill-opacity:1;stroke:#000000;stroke-width:1.21502817;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 37.113903,280.91958 a 11.943909,11.943909 0 0 0 -11.943581,11.94359 h 23.88762 A 11.943909,11.943909 0 0 0 37.113903,280.91958 Z"
|
||||
id="path4732"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="display:inline;opacity:1;fill:#f1cd6b;fill-opacity:1;stroke:#000000;stroke-width:1.21502817;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 62.767977,281.00369 A 11.943909,11.943909 0 0 0 50.824392,292.94728 H 74.712015 A 11.943909,11.943909 0 0 0 62.767977,281.00369 Z"
|
||||
id="path4732-2" />
|
||||
<path
|
||||
style="fill:#dd9829;fill-opacity:1;stroke:#000000;stroke-width:1.21502817;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 45.440938,246.0794 c 0,0 2.139571,9.72892 5.947619,9.7541 3.839356,0.0254 6.126056,-9.7541 6.126056,-9.7541 l -6.140761,-8.69398 z"
|
||||
id="path4721"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="caccc" />
|
||||
<path
|
||||
style="opacity:1;fill:#fedf89;fill-opacity:1;stroke:#000000;stroke-width:1.95828712;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 34.506691,216.30236 a 18.692076,18.692076 0 0 0 -18.692058,18.69205 18.692076,18.692076 0 0 0 18.692058,18.69206 18.692076,18.692076 0 0 0 16.928008,-10.81661 18.692078,18.692078 0 0 0 16.968945,10.90079 18.692078,18.692078 0 0 0 18.69206,-18.69206 18.692078,18.692078 0 0 0 -18.69206,-18.69252 18.692078,18.692078 0 0 0 -16.929847,10.81983 18.692076,18.692076 0 0 0 -16.967106,-10.90354 z"
|
||||
id="path4607"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#fffefc;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 64.955116,221.31207 a 13.794374,13.429993 0 0 0 -13.301924,9.88009 13.794374,13.429993 0 0 0 -13.277543,-9.79636 13.794374,13.429993 0 0 0 -13.794111,13.43025 13.794374,13.429993 0 0 0 13.794111,13.4298 13.794374,13.429993 0 0 0 13.301003,-9.92837 13.794374,13.429993 0 0 0 13.278464,9.8442 13.794374,13.429993 0 0 0 13.794572,-13.4298 13.794374,13.429993 0 0 0 -13.794572,-13.42981 z"
|
||||
id="path4609"
|
||||
inkscape:connector-curvature="0" />
|
||||
<ellipse
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4611"
|
||||
cx="40.394402"
|
||||
cy="235.28864"
|
||||
rx="8.9999876"
|
||||
ry="8.8738194" />
|
||||
<circle
|
||||
style="opacity:1;fill:#fffefc;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4609-3"
|
||||
cy="230.89021"
|
||||
cx="35.68087"
|
||||
r="5.6899729" />
|
||||
<ellipse
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4611-9"
|
||||
cx="-63.020538"
|
||||
cy="235.37274"
|
||||
rx="8.9999876"
|
||||
ry="8.8738194"
|
||||
transform="scale(-1,1)" />
|
||||
<circle
|
||||
style="opacity:1;fill:#fffefc;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4609-3-1"
|
||||
cx="-67.734055"
|
||||
cy="230.97433"
|
||||
transform="scale(-1,1)"
|
||||
r="5.0387244" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 10 KiB |
48
auth_test.go
48
auth_test.go
|
@ -1,48 +0,0 @@
|
|||
package owl_test
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetRedirctUrisLink(t *testing.T) {
|
||||
html := []byte("<link rel=\"redirect_uri\" href=\"http://example.com/redirect\" />")
|
||||
parser := &owl.OwlHtmlParser{}
|
||||
uris, err := parser.GetRedirctUris(constructResponse(html))
|
||||
|
||||
assertions.AssertNoError(t, err, "Unable to parse feed")
|
||||
|
||||
assertions.AssertArrayContains(t, uris, "http://example.com/redirect")
|
||||
}
|
||||
|
||||
func TestGetRedirctUrisLinkMultiple(t *testing.T) {
|
||||
html := []byte(`
|
||||
<link rel="redirect_uri" href="http://example.com/redirect1" />
|
||||
<link rel="redirect_uri" href="http://example.com/redirect2" />
|
||||
<link rel="redirect_uri" href="http://example.com/redirect3" />
|
||||
<link rel="foo" href="http://example.com/redirect4" />
|
||||
<link href="http://example.com/redirect5" />
|
||||
`)
|
||||
parser := &owl.OwlHtmlParser{}
|
||||
uris, err := parser.GetRedirctUris(constructResponse(html))
|
||||
|
||||
assertions.AssertNoError(t, err, "Unable to parse feed")
|
||||
|
||||
assertions.AssertArrayContains(t, uris, "http://example.com/redirect1")
|
||||
assertions.AssertArrayContains(t, uris, "http://example.com/redirect2")
|
||||
assertions.AssertArrayContains(t, uris, "http://example.com/redirect3")
|
||||
assertions.AssertLen(t, uris, 3)
|
||||
}
|
||||
|
||||
func TestGetRedirectUrisLinkHeader(t *testing.T) {
|
||||
html := []byte("")
|
||||
parser := &owl.OwlHtmlParser{}
|
||||
resp := constructResponse(html)
|
||||
resp.Header = http.Header{"Link": []string{"<http://example.com/redirect>; rel=\"redirect_uri\""}}
|
||||
uris, err := parser.GetRedirctUris(resp)
|
||||
|
||||
assertions.AssertNoError(t, err, "Unable to parse feed")
|
||||
assertions.AssertArrayContains(t, uris, "http://example.com/redirect")
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var domain string
|
||||
var singleUser string
|
||||
var unsafe bool
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(initCmd)
|
||||
|
||||
initCmd.PersistentFlags().StringVar(&domain, "domain", "http://localhost:8080", "Domain to use")
|
||||
initCmd.PersistentFlags().StringVar(&singleUser, "single-user", "", "Use single user mode with given username")
|
||||
initCmd.PersistentFlags().BoolVar(&unsafe, "unsafe", false, "Allow raw html")
|
||||
}
|
||||
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Creates a new repository",
|
||||
Long: `Creates a new repository`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
_, err := owl.CreateRepository(repoPath, owl.RepoConfig{
|
||||
Domain: domain,
|
||||
SingleUser: singleUser,
|
||||
AllowRawHtml: unsafe,
|
||||
})
|
||||
if err != nil {
|
||||
println("Error creating repository: ", err.Error())
|
||||
} else {
|
||||
println("Repository created: ", repoPath)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var repoPath string
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "owl",
|
||||
Short: "Owl Blogs is a not so static blog generator",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&repoPath, "repo", ".", "Path to the repository to use.")
|
||||
rootCmd.PersistentFlags().StringVar(&user, "user", "", "Username. Required for some commands.")
|
||||
|
||||
}
|
||||
|
||||
func main() {
|
||||
Execute()
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var postTitle string
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(newPostCmd)
|
||||
newPostCmd.PersistentFlags().StringVar(&postTitle, "title", "", "Post title")
|
||||
}
|
||||
|
||||
var newPostCmd = &cobra.Command{
|
||||
Use: "new-post",
|
||||
Short: "Creates a new post",
|
||||
Long: `Creates a new post`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if user == "" {
|
||||
println("Username is required")
|
||||
return
|
||||
}
|
||||
|
||||
if postTitle == "" {
|
||||
println("Post title is required")
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := owl.OpenRepository(repoPath)
|
||||
if err != nil {
|
||||
println("Error opening repository: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
user, err := repo.GetUser(user)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
post, err := user.CreateNewPost(owl.PostMeta{Type: "article", Title: postTitle, Draft: true}, "")
|
||||
if err != nil {
|
||||
println("Error creating post: ", err.Error())
|
||||
} else {
|
||||
println("Post created: ", postTitle)
|
||||
println("Edit: ", post.ContentFile())
|
||||
}
|
||||
},
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var user string
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(newUserCmd)
|
||||
}
|
||||
|
||||
var newUserCmd = &cobra.Command{
|
||||
Use: "new-user",
|
||||
Short: "Creates a new user",
|
||||
Long: `Creates a new user`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if user == "" {
|
||||
println("Username is required")
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := owl.OpenRepository(repoPath)
|
||||
if err != nil {
|
||||
println("Error opening repository: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, err = repo.CreateUser(user)
|
||||
if err != nil {
|
||||
println("Error creating user: ", err.Error())
|
||||
} else {
|
||||
println("User created: ", user)
|
||||
}
|
||||
},
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"h4kor/owl-blogs"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(resetPasswordCmd)
|
||||
}
|
||||
|
||||
var resetPasswordCmd = &cobra.Command{
|
||||
Use: "reset-password",
|
||||
Short: "Reset the password for a user",
|
||||
Long: `Reset the password for a user`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if user == "" {
|
||||
println("Username is required")
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := owl.OpenRepository(repoPath)
|
||||
if err != nil {
|
||||
println("Error opening repository: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
user, err := repo.GetUser(user)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// generate a random password and print it
|
||||
password := owl.GenerateRandomString(16)
|
||||
user.ResetPassword(password)
|
||||
|
||||
fmt.Println("User: ", user.Name())
|
||||
fmt.Println("New Password: ", password)
|
||||
|
||||
},
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
web "h4kor/owl-blogs/cmd/owl/web"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var port int
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(webCmd)
|
||||
|
||||
webCmd.PersistentFlags().IntVar(&port, "port", 8080, "Port to use")
|
||||
}
|
||||
|
||||
var webCmd = &cobra.Command{
|
||||
Use: "web",
|
||||
Short: "Start the web server",
|
||||
Long: `Start the web server`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
web.StartServer(repoPath, port)
|
||||
},
|
||||
}
|
|
@ -1,191 +0,0 @@
|
|||
package web_test
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
main "h4kor/owl-blogs/cmd/owl/web"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRedirectOnAliases(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "post-1"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - /foo/bar\n"
|
||||
content += " - /foo/baz\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", "/foo/bar", nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusMovedPermanently)
|
||||
// Check that Location header is set correctly
|
||||
assertions.AssertEqual(t, rr.Header().Get("Location"), post.UrlPath())
|
||||
}
|
||||
|
||||
func TestNoRedirectOnNonExistingAliases(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "post-1"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - /foo/bar\n"
|
||||
content += " - /foo/baz\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", "/foo/bar2", nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusNotFound)
|
||||
|
||||
}
|
||||
|
||||
func TestNoRedirectIfValidPostUrl(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "post-1"}, "")
|
||||
post2, _ := user.CreateNewPost(owl.PostMeta{Title: "post-2"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - " + post2.UrlPath() + "\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", post2.UrlPath(), nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
|
||||
}
|
||||
|
||||
func TestRedirectIfInvalidPostUrl(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "post-1"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - " + user.UrlPath() + "posts/not-a-real-post/" + "\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", user.UrlPath()+"posts/not-a-real-post/", nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusMovedPermanently)
|
||||
|
||||
}
|
||||
|
||||
func TestRedirectIfInvalidUserUrl(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "post-1"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - /user/not-real/ \n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", "/user/not-real/", nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusMovedPermanently)
|
||||
|
||||
}
|
||||
|
||||
func TestRedirectIfInvalidMediaUrl(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "post-1"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - " + post.UrlMediaPath("not-real") + "\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", post.UrlMediaPath("not-real"), nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusMovedPermanently)
|
||||
|
||||
}
|
||||
|
||||
func TestDeepAliasInSingleUserMode(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{SingleUser: "test-1"})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "post-1"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Create tileable textures with GIMP\n"
|
||||
content += "author: h4kor\n"
|
||||
content += "type: post\n"
|
||||
content += "date: Tue, 13 Sep 2016 16:19:09 +0000\n"
|
||||
content += "aliases:\n"
|
||||
content += " - /2016/09/13/create-tileable-textures-with-gimp/\n"
|
||||
content += "categories:\n"
|
||||
content += " - GameDev\n"
|
||||
content += "tags:\n"
|
||||
content += " - gamedev\n"
|
||||
content += " - textures\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", "/2016/09/13/create-tileable-textures-with-gimp/", nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusMovedPermanently)
|
||||
|
||||
}
|
|
@ -1,396 +0,0 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"h4kor/owl-blogs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
type IndieauthMetaDataResponse struct {
|
||||
Issuer string `json:"issuer"`
|
||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
|
||||
ScopesSupported []string `json:"scopes_supported"`
|
||||
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||
GrantTypesSupported []string `json:"grant_types_supported"`
|
||||
}
|
||||
|
||||
type MeProfileResponse struct {
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
Photo string `json:"photo"`
|
||||
}
|
||||
type MeResponse struct {
|
||||
Me string `json:"me"`
|
||||
Profile MeProfileResponse `json:"profile"`
|
||||
}
|
||||
|
||||
type AccessTokenResponse struct {
|
||||
Me string `json:"me"`
|
||||
TokenType string `json:"token_type"`
|
||||
AccessToken string `json:"access_token"`
|
||||
Scope string `json:"scope"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
func jsonResponse(w http.ResponseWriter, response interface{}) {
|
||||
jsonData, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
println("Error marshalling json: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Internal server error"))
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Write(jsonData)
|
||||
}
|
||||
|
||||
func userAuthMetadataHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
jsonResponse(w, IndieauthMetaDataResponse{
|
||||
Issuer: user.FullUrl(),
|
||||
AuthorizationEndpoint: user.AuthUrl(),
|
||||
TokenEndpoint: user.TokenUrl(),
|
||||
CodeChallengeMethodsSupported: []string{"S256", "plain"},
|
||||
ScopesSupported: []string{"profile"},
|
||||
ResponseTypesSupported: []string{"code"},
|
||||
GrantTypesSupported: []string{"authorization_code"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func userAuthHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
// get me, cleint_id, redirect_uri, state and response_type from query
|
||||
me := r.URL.Query().Get("me")
|
||||
clientId := r.URL.Query().Get("client_id")
|
||||
redirectUri := r.URL.Query().Get("redirect_uri")
|
||||
state := r.URL.Query().Get("state")
|
||||
responseType := r.URL.Query().Get("response_type")
|
||||
codeChallenge := r.URL.Query().Get("code_challenge")
|
||||
codeChallengeMethod := r.URL.Query().Get("code_challenge_method")
|
||||
scope := r.URL.Query().Get("scope")
|
||||
|
||||
// check if request is valid
|
||||
missing_params := []string{}
|
||||
if clientId == "" {
|
||||
missing_params = append(missing_params, "client_id")
|
||||
}
|
||||
if redirectUri == "" {
|
||||
missing_params = append(missing_params, "redirect_uri")
|
||||
}
|
||||
if responseType == "" {
|
||||
missing_params = append(missing_params, "response_type")
|
||||
}
|
||||
if len(missing_params) > 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Missing parameters",
|
||||
Message: "Missing parameters: " + strings.Join(missing_params, ", "),
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
if responseType == "id" {
|
||||
responseType = "code"
|
||||
}
|
||||
if responseType != "code" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Invalid response_type",
|
||||
Message: "Must be 'code' ('id' converted to 'code' for legacy support).",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
if codeChallengeMethod != "" && (codeChallengeMethod != "S256" && codeChallengeMethod != "plain") {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Invalid code_challenge_method",
|
||||
Message: "Must be 'S256' or 'plain'.",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
|
||||
client_id_url, err := url.Parse(clientId)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Invalid client_id",
|
||||
Message: "Invalid client_id: " + clientId,
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
redirect_uri_url, err := url.Parse(redirectUri)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Invalid redirect_uri",
|
||||
Message: "Invalid redirect_uri: " + redirectUri,
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
if client_id_url.Host != redirect_uri_url.Host || client_id_url.Scheme != redirect_uri_url.Scheme {
|
||||
// check if redirect_uri is registered
|
||||
resp, _ := repo.HttpClient.Get(clientId)
|
||||
registered_redirects, _ := repo.Parser.GetRedirctUris(resp)
|
||||
is_registered := false
|
||||
for _, registered_redirect := range registered_redirects {
|
||||
if registered_redirect == redirectUri {
|
||||
// redirect_uri is registered
|
||||
is_registered = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !is_registered {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Invalid redirect_uri",
|
||||
Message: redirectUri + " is not registered for " + clientId,
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Double Submit Cookie Pattern
|
||||
// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
|
||||
csrfToken := owl.GenerateRandomString(32)
|
||||
cookie := http.Cookie{
|
||||
Name: "csrf_token",
|
||||
Value: csrfToken,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
http.SetCookie(w, &cookie)
|
||||
|
||||
reqData := owl.AuthRequestData{
|
||||
Me: me,
|
||||
ClientId: clientId,
|
||||
RedirectUri: redirectUri,
|
||||
State: state,
|
||||
Scope: scope,
|
||||
ResponseType: responseType,
|
||||
CodeChallenge: codeChallenge,
|
||||
CodeChallengeMethod: codeChallengeMethod,
|
||||
User: user,
|
||||
CsrfToken: csrfToken,
|
||||
}
|
||||
|
||||
html, err := owl.RenderUserAuthPage(reqData)
|
||||
if err != nil {
|
||||
println("Error rendering auth page: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Internal Server Error",
|
||||
Message: "Internal Server Error",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
}
|
||||
|
||||
func verifyAuthCodeRequest(user owl.User, w http.ResponseWriter, r *http.Request) (bool, owl.AuthCode) {
|
||||
// get form data from post request
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
println("Error parsing form: ", err.Error())
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Error parsing form"))
|
||||
return false, owl.AuthCode{}
|
||||
}
|
||||
code := r.Form.Get("code")
|
||||
client_id := r.Form.Get("client_id")
|
||||
redirect_uri := r.Form.Get("redirect_uri")
|
||||
code_verifier := r.Form.Get("code_verifier")
|
||||
|
||||
// check if request is valid
|
||||
valid, authCode := user.VerifyAuthCode(code, client_id, redirect_uri, code_verifier)
|
||||
if !valid {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Invalid code"))
|
||||
}
|
||||
return valid, authCode
|
||||
}
|
||||
|
||||
func userAuthProfileHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
valid, _ := verifyAuthCodeRequest(user, w, r)
|
||||
if valid {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
jsonResponse(w, MeResponse{
|
||||
Me: user.FullUrl(),
|
||||
Profile: MeProfileResponse{
|
||||
Name: user.Name(),
|
||||
Url: user.FullUrl(),
|
||||
Photo: user.AvatarUrl(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func userAuthTokenHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
valid, authCode := verifyAuthCodeRequest(user, w, r)
|
||||
if valid {
|
||||
if authCode.Scope == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Empty scope, no token issued"))
|
||||
return
|
||||
}
|
||||
|
||||
accessToken, duration, err := user.GenerateAccessToken(authCode)
|
||||
if err != nil {
|
||||
println("Error generating access token: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Internal server error"))
|
||||
return
|
||||
}
|
||||
jsonResponse(w, AccessTokenResponse{
|
||||
Me: user.FullUrl(),
|
||||
TokenType: "Bearer",
|
||||
AccessToken: accessToken,
|
||||
Scope: authCode.Scope,
|
||||
ExpiresIn: duration,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func userAuthVerifyHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// get form data from post request
|
||||
err = r.ParseForm()
|
||||
if err != nil {
|
||||
println("Error parsing form: ", err.Error())
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Error parsing form",
|
||||
Message: "Error parsing form",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
password := r.FormValue("password")
|
||||
client_id := r.FormValue("client_id")
|
||||
redirect_uri := r.FormValue("redirect_uri")
|
||||
response_type := r.FormValue("response_type")
|
||||
state := r.FormValue("state")
|
||||
code_challenge := r.FormValue("code_challenge")
|
||||
code_challenge_method := r.FormValue("code_challenge_method")
|
||||
scope := r.FormValue("scope")
|
||||
|
||||
// CSRF check
|
||||
formCsrfToken := r.FormValue("csrf_token")
|
||||
cookieCsrfToken, err := r.Cookie("csrf_token")
|
||||
|
||||
if err != nil {
|
||||
println("Error getting csrf token from cookie: ", err.Error())
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "CSRF Error",
|
||||
Message: "Error getting csrf token from cookie",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
if formCsrfToken != cookieCsrfToken.Value {
|
||||
println("Invalid csrf token")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "CSRF Error",
|
||||
Message: "Invalid csrf token",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
|
||||
password_valid := user.VerifyPassword(password)
|
||||
if !password_valid {
|
||||
redirect := fmt.Sprintf(
|
||||
"%s?error=invalid_password&client_id=%s&redirect_uri=%s&response_type=%s&state=%s",
|
||||
user.AuthUrl(), client_id, redirect_uri, response_type, state,
|
||||
)
|
||||
if code_challenge != "" {
|
||||
redirect += fmt.Sprintf("&code_challenge=%s&code_challenge_method=%s", code_challenge, code_challenge_method)
|
||||
}
|
||||
http.Redirect(w, r,
|
||||
redirect,
|
||||
http.StatusFound,
|
||||
)
|
||||
return
|
||||
} else {
|
||||
// password is valid, generate code
|
||||
code, err := user.GenerateAuthCode(
|
||||
client_id, redirect_uri, code_challenge, code_challenge_method, scope)
|
||||
if err != nil {
|
||||
println("Error generating code: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Internal Server Error",
|
||||
Message: "Error generating code",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r,
|
||||
fmt.Sprintf(
|
||||
"%s?code=%s&state=%s&iss=%s",
|
||||
redirect_uri, code, state,
|
||||
user.FullUrl(),
|
||||
),
|
||||
http.StatusFound,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,428 +0,0 @@
|
|||
package web_test
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
main "h4kor/owl-blogs/cmd/owl/web"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"h4kor/owl-blogs/test/mocks"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAuthPostWrongPassword(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("password", "wrongpassword")
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("response_type", "code")
|
||||
form.Add("state", "test_state")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
|
||||
req, err := http.NewRequest("POST", user.AuthUrl()+"verify/", strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusFound)
|
||||
assertions.AssertContains(t, rr.Header().Get("Location"), "error=invalid_password")
|
||||
}
|
||||
|
||||
func TestAuthPostCorrectPassword(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("password", "testpassword")
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("response_type", "code")
|
||||
form.Add("state", "test_state")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
req, err := http.NewRequest("POST", user.AuthUrl()+"verify/", strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusFound)
|
||||
assertions.AssertContains(t, rr.Header().Get("Location"), "code=")
|
||||
assertions.AssertContains(t, rr.Header().Get("Location"), "state=test_state")
|
||||
assertions.AssertContains(t, rr.Header().Get("Location"), "iss="+user.FullUrl())
|
||||
assertions.AssertContains(t, rr.Header().Get("Location"), "http://example.com/response")
|
||||
}
|
||||
|
||||
func TestAuthPostWithIncorrectCode(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
user.GenerateAuthCode("http://example.com", "http://example.com/response", "", "", "profile")
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("code", "wrongcode")
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("grant_type", "authorization_code")
|
||||
req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func TestAuthPostWithCorrectCode(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", "", "", "profile")
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("code", code)
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("grant_type", "authorization_code")
|
||||
req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.Header.Add("Accept", "application/json")
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
// parse response as json
|
||||
type responseType struct {
|
||||
Me string `json:"me"`
|
||||
}
|
||||
var response responseType
|
||||
json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
assertions.AssertEqual(t, response.Me, user.FullUrl())
|
||||
|
||||
}
|
||||
|
||||
func TestAuthPostWithCorrectCodeAndPKCE(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
// Create Request and Response
|
||||
code_verifier := "test_code_verifier"
|
||||
// create code challenge
|
||||
h := sha256.New()
|
||||
h.Write([]byte(code_verifier))
|
||||
code_challenge := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||
code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", code_challenge, "S256", "profile")
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("code", code)
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("grant_type", "authorization_code")
|
||||
form.Add("code_verifier", code_verifier)
|
||||
req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.Header.Add("Accept", "application/json")
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
// parse response as json
|
||||
type responseType struct {
|
||||
Me string `json:"me"`
|
||||
}
|
||||
var response responseType
|
||||
json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
assertions.AssertEqual(t, response.Me, user.FullUrl())
|
||||
|
||||
}
|
||||
|
||||
func TestAuthPostWithCorrectCodeAndWrongPKCE(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
// Create Request and Response
|
||||
code_verifier := "test_code_verifier"
|
||||
// create code challenge
|
||||
h := sha256.New()
|
||||
h.Write([]byte(code_verifier + "wrong"))
|
||||
code_challenge := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||
code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", code_challenge, "S256", "profile")
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("code", code)
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("grant_type", "authorization_code")
|
||||
form.Add("code_verifier", code_verifier)
|
||||
req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.Header.Add("Accept", "application/json")
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func TestAuthPostWithCorrectCodePKCEPlain(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
// Create Request and Response
|
||||
code_verifier := "test_code_verifier"
|
||||
code_challenge := code_verifier
|
||||
code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", code_challenge, "plain", "profile")
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("code", code)
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("grant_type", "authorization_code")
|
||||
form.Add("code_verifier", code_verifier)
|
||||
req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.Header.Add("Accept", "application/json")
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestAuthPostWithCorrectCodePKCEPlainWrong(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
// Create Request and Response
|
||||
code_verifier := "test_code_verifier"
|
||||
code_challenge := code_verifier + "wrong"
|
||||
code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", code_challenge, "plain", "profile")
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("code", code)
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("grant_type", "authorization_code")
|
||||
form.Add("code_verifier", code_verifier)
|
||||
req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.Header.Add("Accept", "application/json")
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func TestAuthRedirectUriNotSet(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
repo.HttpClient = &mocks.MockHttpClient{}
|
||||
repo.Parser = &mocks.MockParseLinksHtmlParser{
|
||||
Links: []string{"http://example2.com/response"},
|
||||
}
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("password", "wrongpassword")
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example2.com/response_not_set")
|
||||
form.Add("response_type", "code")
|
||||
form.Add("state", "test_state")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
|
||||
req, err := http.NewRequest("GET", user.AuthUrl()+"?"+form.Encode(), nil)
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestAuthRedirectUriSet(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
repo.HttpClient = &mocks.MockHttpClient{}
|
||||
repo.Parser = &mocks.MockParseLinksHtmlParser{
|
||||
Links: []string{"http://example.com/response"},
|
||||
}
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("password", "wrongpassword")
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("response_type", "code")
|
||||
form.Add("state", "test_state")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
|
||||
req, err := http.NewRequest("GET", user.AuthUrl()+"?"+form.Encode(), nil)
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestAuthRedirectUriSameHost(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
repo.HttpClient = &mocks.MockHttpClient{}
|
||||
repo.Parser = &mocks.MockParseLinksHtmlParser{
|
||||
Links: []string{},
|
||||
}
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("password", "wrongpassword")
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("response_type", "code")
|
||||
form.Add("state", "test_state")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
|
||||
req, err := http.NewRequest("GET", user.AuthUrl()+"?"+form.Encode(), nil)
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestAccessTokenCorrectPassword(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", "", "", "profile create")
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("code", code)
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("grant_type", "authorization_code")
|
||||
req, err := http.NewRequest("POST", user.AuthUrl()+"token/", strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
// parse response as json
|
||||
type responseType struct {
|
||||
Me string `json:"me"`
|
||||
TokenType string `json:"token_type"`
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
var response responseType
|
||||
json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
assertions.AssertEqual(t, response.Me, user.FullUrl())
|
||||
assertions.AssertEqual(t, response.TokenType, "Bearer")
|
||||
assertions.AssertEqual(t, response.Scope, "profile create")
|
||||
assertions.Assert(t, response.ExpiresIn > 0, "ExpiresIn should be greater than 0")
|
||||
assertions.Assert(t, len(response.AccessToken) > 0, "AccessToken should be greater than 0")
|
||||
}
|
||||
|
||||
func TestAccessTokenWithIncorrectCode(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
user.GenerateAuthCode("http://example.com", "http://example.com/response", "", "", "profile")
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("code", "wrongcode")
|
||||
form.Add("client_id", "http://example.com")
|
||||
form.Add("redirect_uri", "http://example.com/response")
|
||||
form.Add("grant_type", "authorization_code")
|
||||
req, err := http.NewRequest("POST", user.AuthUrl()+"token/", strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func TestIndieauthMetadata(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
req, _ := http.NewRequest("GET", user.IndieauthMetadataUrl(), nil)
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
// parse response as json
|
||||
type responseType struct {
|
||||
Issuer string `json:"issuer"`
|
||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
|
||||
ScopesSupported []string `json:"scopes_supported"`
|
||||
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||
GrantTypesSupported []string `json:"grant_types_supported"`
|
||||
}
|
||||
var response responseType
|
||||
json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
assertions.AssertEqual(t, response.Issuer, user.FullUrl())
|
||||
assertions.AssertEqual(t, response.AuthorizationEndpoint, user.AuthUrl())
|
||||
assertions.AssertEqual(t, response.TokenEndpoint, user.TokenUrl())
|
||||
}
|
|
@ -1,364 +0,0 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"h4kor/owl-blogs"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
func isUserLoggedIn(user *owl.User, r *http.Request) bool {
|
||||
sessionCookie, err := r.Cookie("session")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return user.ValidateSession(sessionCookie.Value)
|
||||
}
|
||||
|
||||
func setCSRFCookie(w http.ResponseWriter) string {
|
||||
csrfToken := owl.GenerateRandomString(32)
|
||||
cookie := http.Cookie{
|
||||
Name: "csrf_token",
|
||||
Value: csrfToken,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
http.SetCookie(w, &cookie)
|
||||
return csrfToken
|
||||
}
|
||||
|
||||
func checkCSRF(r *http.Request) bool {
|
||||
// CSRF check
|
||||
formCsrfToken := r.FormValue("csrf_token")
|
||||
cookieCsrfToken, err := r.Cookie("csrf_token")
|
||||
|
||||
if err != nil {
|
||||
println("Error getting csrf token from cookie: ", err.Error())
|
||||
return false
|
||||
}
|
||||
if formCsrfToken != cookieCsrfToken.Value {
|
||||
println("Invalid csrf token")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func userLoginGetHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if isUserLoggedIn(&user, r) {
|
||||
http.Redirect(w, r, user.EditorUrl(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
csrfToken := setCSRFCookie(w)
|
||||
|
||||
// get error from query
|
||||
error_type := r.URL.Query().Get("error")
|
||||
|
||||
html, err := owl.RenderLoginPage(user, error_type, csrfToken)
|
||||
if err != nil {
|
||||
println("Error rendering login page: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Internal server error",
|
||||
Message: "Internal server error",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
}
|
||||
|
||||
func userLoginPostHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
err = r.ParseForm()
|
||||
if err != nil {
|
||||
println("Error parsing form: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Internal server error",
|
||||
Message: "Internal server error",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
|
||||
// CSRF check
|
||||
if !checkCSRF(r) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "CSRF Error",
|
||||
Message: "Invalid csrf token",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
|
||||
password := r.Form.Get("password")
|
||||
if password == "" || !user.VerifyPassword(password) {
|
||||
http.Redirect(w, r, user.EditorLoginUrl()+"?error=wrong_password", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
// set session cookie
|
||||
cookie := http.Cookie{
|
||||
Name: "session",
|
||||
Value: user.CreateNewSession(),
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(30 * 24 * time.Hour),
|
||||
HttpOnly: true,
|
||||
}
|
||||
http.SetCookie(w, &cookie)
|
||||
http.Redirect(w, r, user.EditorUrl(), http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func userEditorGetHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if !isUserLoggedIn(&user, r) {
|
||||
http.Redirect(w, r, user.EditorLoginUrl(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
csrfToken := setCSRFCookie(w)
|
||||
html, err := owl.RenderEditorPage(user, csrfToken)
|
||||
if err != nil {
|
||||
println("Error rendering editor page: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Internal server error",
|
||||
Message: "Internal server error",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
}
|
||||
|
||||
func userEditorPostHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if !isUserLoggedIn(&user, r) {
|
||||
http.Redirect(w, r, user.EditorLoginUrl(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Split(r.Header.Get("Content-Type"), ";")[0] == "multipart/form-data" {
|
||||
err = r.ParseMultipartForm(32 << 20)
|
||||
} else {
|
||||
err = r.ParseForm()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
println("Error parsing form: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Internal server error",
|
||||
Message: "Internal server error",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
|
||||
// CSRF check
|
||||
if !checkCSRF(r) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "CSRF Error",
|
||||
Message: "Invalid csrf token",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
|
||||
// get form values
|
||||
post_type := r.Form.Get("type")
|
||||
title := r.Form.Get("title")
|
||||
description := r.Form.Get("description")
|
||||
content := strings.ReplaceAll(r.Form.Get("content"), "\r", "")
|
||||
draft := r.Form.Get("draft")
|
||||
|
||||
// recipe values
|
||||
recipe_yield := r.Form.Get("yield")
|
||||
recipe_ingredients := strings.ReplaceAll(r.Form.Get("ingredients"), "\r", "")
|
||||
recipe_duration := r.Form.Get("duration")
|
||||
|
||||
// conditional values
|
||||
reply_url := r.Form.Get("reply_url")
|
||||
bookmark_url := r.Form.Get("bookmark_url")
|
||||
|
||||
// photo values
|
||||
var photo_file multipart.File
|
||||
var photo_header *multipart.FileHeader
|
||||
if strings.Split(r.Header.Get("Content-Type"), ";")[0] == "multipart/form-data" {
|
||||
photo_file, photo_header, err = r.FormFile("photo")
|
||||
if err != nil && err != http.ErrMissingFile {
|
||||
println("Error getting photo file: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Internal server error",
|
||||
Message: "Internal server error",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// validate form values
|
||||
if post_type == "" {
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Missing post type",
|
||||
Message: "Post type is required",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
if (post_type == "article" || post_type == "page" || post_type == "recipe") && title == "" {
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Missing Title",
|
||||
Message: "Articles and Pages must have a title",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
if post_type == "reply" && reply_url == "" {
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Missing URL",
|
||||
Message: "You must provide a URL to reply to",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
if post_type == "bookmark" && bookmark_url == "" {
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Missing URL",
|
||||
Message: "You must provide a URL to bookmark",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
if post_type == "photo" && photo_file == nil {
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Missing Photo",
|
||||
Message: "You must provide a photo to upload",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: scrape reply_url for title and description
|
||||
// TODO: scrape bookmark_url for title and description
|
||||
|
||||
// create post
|
||||
meta := owl.PostMeta{
|
||||
Type: post_type,
|
||||
Title: title,
|
||||
Description: description,
|
||||
Draft: draft == "on",
|
||||
Date: time.Now(),
|
||||
Reply: owl.ReplyData{
|
||||
Url: reply_url,
|
||||
},
|
||||
Bookmark: owl.BookmarkData{
|
||||
Url: bookmark_url,
|
||||
},
|
||||
Recipe: owl.RecipeData{
|
||||
Yield: recipe_yield,
|
||||
Ingredients: strings.Split(recipe_ingredients, "\n"),
|
||||
Duration: recipe_duration,
|
||||
},
|
||||
}
|
||||
|
||||
if photo_file != nil {
|
||||
meta.PhotoPath = photo_header.Filename
|
||||
}
|
||||
|
||||
post, err := user.CreateNewPost(meta, content)
|
||||
|
||||
// save photo
|
||||
if photo_file != nil {
|
||||
println("Saving photo: ", photo_header.Filename)
|
||||
photo_path := path.Join(post.MediaDir(), photo_header.Filename)
|
||||
media_file, err := os.Create(photo_path)
|
||||
if err != nil {
|
||||
println("Error creating photo file: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Internal server error",
|
||||
Message: "Internal server error",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
defer media_file.Close()
|
||||
io.Copy(media_file, photo_file)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
println("Error creating post: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Internal server error",
|
||||
Message: "Internal server error",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
|
||||
// redirect to post
|
||||
if !post.Meta().Draft {
|
||||
// scan for webmentions
|
||||
post.ScanForLinks()
|
||||
webmentions := post.OutgoingWebmentions()
|
||||
println("Found ", len(webmentions), " links")
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(webmentions))
|
||||
for _, mention := range post.OutgoingWebmentions() {
|
||||
go func(mention owl.WebmentionOut) {
|
||||
fmt.Printf("Sending webmention to %s", mention.Target)
|
||||
defer wg.Done()
|
||||
post.SendWebmention(mention)
|
||||
}(mention)
|
||||
}
|
||||
wg.Wait()
|
||||
http.Redirect(w, r, post.FullUrl(), http.StatusFound)
|
||||
} else {
|
||||
http.Redirect(w, r, user.EditorUrl(), http.StatusFound)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,346 +0,0 @@
|
|||
package web_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"h4kor/owl-blogs"
|
||||
main "h4kor/owl-blogs/cmd/owl/web"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"h4kor/owl-blogs/test/mocks"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type CountMockHttpClient struct {
|
||||
InvokedGet int
|
||||
InvokedPost int
|
||||
InvokedPostForm int
|
||||
}
|
||||
|
||||
func (c *CountMockHttpClient) Get(url string) (resp *http.Response, err error) {
|
||||
c.InvokedGet++
|
||||
return &http.Response{}, nil
|
||||
}
|
||||
|
||||
func (c *CountMockHttpClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
|
||||
c.InvokedPost++
|
||||
return &http.Response{}, nil
|
||||
}
|
||||
|
||||
func (c *CountMockHttpClient) PostForm(url string, data url.Values) (resp *http.Response, err error) {
|
||||
c.InvokedPostForm++
|
||||
return &http.Response{}, nil
|
||||
}
|
||||
|
||||
func TestLoginWrongPassword(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("password", "wrongpassword")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
|
||||
req, err := http.NewRequest("POST", user.EditorLoginUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
// check redirect to login page
|
||||
|
||||
assertions.AssertEqual(t, rr.Header().Get("Location"), user.EditorLoginUrl()+"?error=wrong_password")
|
||||
}
|
||||
|
||||
func TestLoginCorrectPassword(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("password", "testpassword")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
|
||||
req, err := http.NewRequest("POST", user.EditorLoginUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
// check redirect to login page
|
||||
assertions.AssertStatus(t, rr, http.StatusFound)
|
||||
assertions.AssertEqual(t, rr.Header().Get("Location"), user.EditorUrl())
|
||||
}
|
||||
|
||||
func TestEditorWithoutSession(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
user.CreateNewSession()
|
||||
|
||||
req, err := http.NewRequest("GET", user.EditorUrl(), nil)
|
||||
// req.AddCookie(&http.Cookie{Name: "session", Value: sessionId})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusFound)
|
||||
assertions.AssertEqual(t, rr.Header().Get("Location"), user.EditorLoginUrl())
|
||||
|
||||
}
|
||||
|
||||
func TestEditorWithSession(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
sessionId := user.CreateNewSession()
|
||||
|
||||
req, err := http.NewRequest("GET", user.EditorUrl(), nil)
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: sessionId})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestEditorPostWithoutSession(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
user.CreateNewSession()
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("type", "article")
|
||||
form.Add("title", "testtitle")
|
||||
form.Add("content", "testcontent")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
|
||||
req, err := http.NewRequest("POST", user.EditorUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusFound)
|
||||
assertions.AssertEqual(t, rr.Header().Get("Location"), user.EditorLoginUrl())
|
||||
}
|
||||
|
||||
func TestEditorPostWithSession(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
sessionId := user.CreateNewSession()
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("type", "article")
|
||||
form.Add("title", "testtitle")
|
||||
form.Add("content", "testcontent")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
|
||||
req, err := http.NewRequest("POST", user.EditorUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: sessionId})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
posts, _ := user.AllPosts()
|
||||
assertions.AssertEqual(t, len(posts), 1)
|
||||
post := posts[0]
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusFound)
|
||||
assertions.AssertEqual(t, rr.Header().Get("Location"), post.FullUrl())
|
||||
}
|
||||
|
||||
func TestEditorPostWithSessionNote(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
sessionId := user.CreateNewSession()
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("type", "note")
|
||||
form.Add("content", "testcontent")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
|
||||
req, err := http.NewRequest("POST", user.EditorUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: sessionId})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
posts, _ := user.AllPosts()
|
||||
assertions.AssertEqual(t, len(posts), 1)
|
||||
post := posts[0]
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusFound)
|
||||
assertions.AssertEqual(t, rr.Header().Get("Location"), post.FullUrl())
|
||||
}
|
||||
|
||||
func TestEditorSendsWebmentions(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
repo.HttpClient = &CountMockHttpClient{}
|
||||
repo.Parser = &mocks.MockHtmlParser{}
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
mentioned_post, _ := user.CreateNewPost(owl.PostMeta{Title: "test"}, "")
|
||||
|
||||
sessionId := user.CreateNewSession()
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("type", "note")
|
||||
form.Add("content", "[test]("+mentioned_post.FullUrl()+")")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
|
||||
req, err := http.NewRequest("POST", user.EditorUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: sessionId})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
posts, _ := user.AllPosts()
|
||||
assertions.AssertEqual(t, len(posts), 2)
|
||||
post := posts[0]
|
||||
assertions.AssertLen(t, post.OutgoingWebmentions(), 1)
|
||||
assertions.AssertStatus(t, rr, http.StatusFound)
|
||||
assertions.AssertEqual(t, repo.HttpClient.(*CountMockHttpClient).InvokedPostForm, 1)
|
||||
|
||||
}
|
||||
|
||||
func TestEditorPostWithSessionRecipe(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
sessionId := user.CreateNewSession()
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("type", "recipe")
|
||||
form.Add("title", "testtitle")
|
||||
form.Add("yield", "2")
|
||||
form.Add("duration", "1 hour")
|
||||
form.Add("ingredients", "water\nwheat")
|
||||
form.Add("content", "testcontent")
|
||||
form.Add("csrf_token", csrfToken)
|
||||
|
||||
req, err := http.NewRequest("POST", user.EditorUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: sessionId})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
posts, _ := user.AllPosts()
|
||||
assertions.AssertEqual(t, len(posts), 1)
|
||||
post := posts[0]
|
||||
|
||||
assertions.AssertLen(t, post.Meta().Recipe.Ingredients, 2)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusFound)
|
||||
assertions.AssertEqual(t, rr.Header().Get("Location"), post.FullUrl())
|
||||
}
|
||||
|
||||
func TestEditorPostWithSessionPhoto(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
sessionId := user.CreateNewSession()
|
||||
|
||||
csrfToken := "test_csrf_token"
|
||||
|
||||
// read photo from file
|
||||
photo_data, err := ioutil.ReadFile("../../../fixtures/image.png")
|
||||
assertions.AssertNoError(t, err, "Error reading photo")
|
||||
|
||||
// Create Request and Response
|
||||
bodyBuf := &bytes.Buffer{}
|
||||
bodyWriter := multipart.NewWriter(bodyBuf)
|
||||
|
||||
// write photo
|
||||
fileWriter, err := bodyWriter.CreateFormFile("photo", "../../../fixtures/image.png")
|
||||
assertions.AssertNoError(t, err, "Error creating form file")
|
||||
_, err = fileWriter.Write(photo_data)
|
||||
assertions.AssertNoError(t, err, "Error writing photo")
|
||||
|
||||
// write other fields
|
||||
bodyWriter.WriteField("type", "photo")
|
||||
bodyWriter.WriteField("title", "testtitle")
|
||||
bodyWriter.WriteField("content", "testcontent")
|
||||
bodyWriter.WriteField("csrf_token", csrfToken)
|
||||
|
||||
// close body writer
|
||||
err = bodyWriter.Close()
|
||||
assertions.AssertNoError(t, err, "Error closing body writer")
|
||||
|
||||
req, err := http.NewRequest("POST", user.EditorUrl(), bodyBuf)
|
||||
req.Header.Add("Content-Type", "multipart/form-data; boundary="+bodyWriter.Boundary())
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(bodyBuf.Bytes())))
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: sessionId})
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusFound)
|
||||
|
||||
posts, _ := user.AllPosts()
|
||||
assertions.AssertEqual(t, len(posts), 1)
|
||||
post := posts[0]
|
||||
assertions.AssertEqual(t, rr.Header().Get("Location"), post.FullUrl())
|
||||
|
||||
assertions.AssertNotEqual(t, post.Meta().PhotoPath, "")
|
||||
ret_photo_data, err := ioutil.ReadFile(path.Join(post.MediaDir(), post.Meta().PhotoPath))
|
||||
assertions.AssertNoError(t, err, "Error reading photo")
|
||||
assertions.AssertEqual(t, len(photo_data), len(ret_photo_data))
|
||||
if len(photo_data) == len(ret_photo_data) {
|
||||
for i := range photo_data {
|
||||
assertions.AssertEqual(t, photo_data[i], ret_photo_data[i])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,407 +0,0 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"h4kor/owl-blogs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
func getUserFromRepo(repo *owl.Repository, ps httprouter.Params) (owl.User, error) {
|
||||
if config, _ := repo.Config(); config.SingleUser != "" {
|
||||
return repo.GetUser(config.SingleUser)
|
||||
}
|
||||
userName := ps.ByName("user")
|
||||
user, err := repo.GetUser(userName)
|
||||
if err != nil {
|
||||
return owl.User{}, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func repoIndexHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
html, err := owl.RenderUserList(*repo)
|
||||
|
||||
if err != nil {
|
||||
println("Error rendering index: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Internal server error"))
|
||||
return
|
||||
}
|
||||
println("Rendering index")
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
}
|
||||
|
||||
func userIndexHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
html, err := owl.RenderIndexPage(user)
|
||||
if err != nil {
|
||||
println("Error rendering index page: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Internal server error",
|
||||
Message: "Internal server error",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
println("Rendering index page for user", user.Name())
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
}
|
||||
|
||||
func postListHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
listId := ps.ByName("list")
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
list, err := user.GetPostList(listId)
|
||||
|
||||
if err != nil {
|
||||
println("Error getting post list: ", err.Error())
|
||||
notFoundUserHandler(repo, user)(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
html, err := owl.RenderPostList(user, list)
|
||||
if err != nil {
|
||||
println("Error rendering index page: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Internal server error",
|
||||
Message: "Internal server error",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
println("Rendering index page for user", user.Name())
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
}
|
||||
|
||||
func userWebmentionHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("User not found"))
|
||||
return
|
||||
}
|
||||
err = r.ParseForm()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Unable to parse form data"))
|
||||
return
|
||||
}
|
||||
params := r.PostForm
|
||||
target := params["target"]
|
||||
source := params["source"]
|
||||
if len(target) == 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("No target provided"))
|
||||
return
|
||||
}
|
||||
if len(source) == 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("No source provided"))
|
||||
return
|
||||
}
|
||||
|
||||
if len(target[0]) < 7 || (target[0][:7] != "http://" && target[0][:8] != "https://") {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Not a valid target"))
|
||||
return
|
||||
}
|
||||
|
||||
if len(source[0]) < 7 || (source[0][:7] != "http://" && source[0][:8] != "https://") {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Not a valid source"))
|
||||
return
|
||||
}
|
||||
|
||||
if source[0] == target[0] {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("target and source are equal"))
|
||||
return
|
||||
}
|
||||
|
||||
tryAlias := func(target string) owl.Post {
|
||||
parsedTarget, _ := url.Parse(target)
|
||||
aliases, _ := repo.PostAliases()
|
||||
fmt.Printf("aliases %v", aliases)
|
||||
fmt.Printf("parsedTarget %v", parsedTarget)
|
||||
if _, ok := aliases[parsedTarget.Path]; ok {
|
||||
return aliases[parsedTarget.Path]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var aliasPost owl.Post
|
||||
parts := strings.Split(target[0], "/")
|
||||
if len(parts) < 2 {
|
||||
aliasPost = tryAlias(target[0])
|
||||
if aliasPost == nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Not found"))
|
||||
return
|
||||
}
|
||||
}
|
||||
postId := parts[len(parts)-2]
|
||||
foundPost, err := user.GetPost(postId)
|
||||
if err != nil && aliasPost == nil {
|
||||
aliasPost = tryAlias(target[0])
|
||||
if aliasPost == nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Post not found"))
|
||||
return
|
||||
}
|
||||
}
|
||||
if aliasPost != nil {
|
||||
foundPost = aliasPost
|
||||
}
|
||||
err = foundPost.AddIncomingWebmention(source[0])
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Unable to process webmention"))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
w.Write([]byte(""))
|
||||
}
|
||||
}
|
||||
|
||||
func userRSSHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
xml, err := owl.RenderRSSFeed(user)
|
||||
if err != nil {
|
||||
println("Error rendering index page: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Internal server error",
|
||||
Message: "Internal server error",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
println("Rendering index page for user", user.Name())
|
||||
w.Header().Set("Content-Type", "application/rss+xml")
|
||||
w.Write([]byte(xml))
|
||||
}
|
||||
}
|
||||
|
||||
func postHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
postId := ps.ByName("post")
|
||||
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
post, err := user.GetPost(postId)
|
||||
|
||||
if err != nil {
|
||||
println("Error getting post: ", err.Error())
|
||||
notFoundUserHandler(repo, user)(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
meta := post.Meta()
|
||||
if meta.Draft {
|
||||
println("Post is a draft")
|
||||
notFoundUserHandler(repo, user)(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
html, err := owl.RenderPost(post)
|
||||
if err != nil {
|
||||
println("Error rendering post: ", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Internal server error",
|
||||
Message: "Internal server error",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
println("Rendering post", postId)
|
||||
w.Write([]byte(html))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func postMediaHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
postId := ps.ByName("post")
|
||||
filepath := ps.ByName("filepath")
|
||||
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
post, err := user.GetPost(postId)
|
||||
if err != nil {
|
||||
println("Error getting post: ", err.Error())
|
||||
notFoundUserHandler(repo, user)(w, r)
|
||||
return
|
||||
}
|
||||
filepath = path.Join(post.MediaDir(), filepath)
|
||||
if _, err := os.Stat(filepath); err != nil {
|
||||
println("Error getting file: ", err.Error())
|
||||
notFoundUserHandler(repo, user)(w, r)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, filepath)
|
||||
}
|
||||
}
|
||||
|
||||
func userMicropubHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// parse request form
|
||||
err = r.ParseForm()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Bad request"))
|
||||
return
|
||||
}
|
||||
|
||||
// verify access token
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
token = r.Form.Get("access_token")
|
||||
} else {
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
}
|
||||
|
||||
valid, _ := user.ValidateAccessToken(token)
|
||||
if !valid {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
h := r.Form.Get("h")
|
||||
content := r.Form.Get("content")
|
||||
name := r.Form.Get("name")
|
||||
inReplyTo := r.Form.Get("in-reply-to")
|
||||
|
||||
if h != "entry" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Bad request. h must be entry. Got " + h))
|
||||
return
|
||||
}
|
||||
if content == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Bad request. content is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// create post
|
||||
post, err := user.CreateNewPost(
|
||||
owl.PostMeta{
|
||||
Title: name,
|
||||
Reply: owl.ReplyData{
|
||||
Url: inReplyTo,
|
||||
},
|
||||
Date: time.Now(),
|
||||
},
|
||||
content,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Internal server error"))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Location", post.FullUrl())
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func userMediaHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
filepath := ps.ByName("filepath")
|
||||
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
filepath = path.Join(user.MediaDir(), filepath)
|
||||
if _, err := os.Stat(filepath); err != nil {
|
||||
println("Error getting file: ", err.Error())
|
||||
notFoundUserHandler(repo, user)(w, r)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, filepath)
|
||||
}
|
||||
}
|
||||
|
||||
func notFoundHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
aliases, _ := repo.PostAliases()
|
||||
if _, ok := aliases[path]; ok {
|
||||
http.Redirect(w, r, aliases[path].UrlPath(), http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("Not found"))
|
||||
}
|
||||
}
|
||||
|
||||
func notFoundUserHandler(repo *owl.Repository, user owl.User) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
aliases, _ := repo.PostAliases()
|
||||
if _, ok := aliases[path]; ok {
|
||||
http.Redirect(w, r, aliases[path].UrlPath(), http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
|
||||
Error: "Not found",
|
||||
Message: "The page you requested could not be found",
|
||||
})
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
package web_test
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
main "h4kor/owl-blogs/cmd/owl/web"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMicropubMinimalArticle(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
code, _ := user.GenerateAuthCode(
|
||||
"test", "test", "test", "test", "test",
|
||||
)
|
||||
token, _, _ := user.GenerateAccessToken(owl.AuthCode{
|
||||
Code: code,
|
||||
ClientId: "test",
|
||||
RedirectUri: "test",
|
||||
CodeChallenge: "test",
|
||||
CodeChallengeMethod: "test",
|
||||
Scope: "test",
|
||||
})
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("h", "entry")
|
||||
form.Add("name", "Test Article")
|
||||
form.Add("content", "Test Content")
|
||||
|
||||
req, err := http.NewRequest("POST", user.MicropubUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.Header.Add("Authorization", "Bearer "+token)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusCreated)
|
||||
}
|
||||
|
||||
func TestMicropubWithoutName(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
code, _ := user.GenerateAuthCode(
|
||||
"test", "test", "test", "test", "test",
|
||||
)
|
||||
token, _, _ := user.GenerateAccessToken(owl.AuthCode{
|
||||
Code: code,
|
||||
ClientId: "test",
|
||||
RedirectUri: "test",
|
||||
CodeChallenge: "test",
|
||||
CodeChallengeMethod: "test",
|
||||
Scope: "test",
|
||||
})
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("h", "entry")
|
||||
form.Add("content", "Test Content")
|
||||
|
||||
req, err := http.NewRequest("POST", user.MicropubUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.Header.Add("Authorization", "Bearer "+token)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusCreated)
|
||||
loc_header := rr.Header().Get("Location")
|
||||
assertions.Assert(t, loc_header != "", "Location header should be set")
|
||||
}
|
||||
|
||||
func TestMicropubAccessTokenInBody(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
code, _ := user.GenerateAuthCode(
|
||||
"test", "test", "test", "test", "test",
|
||||
)
|
||||
token, _, _ := user.GenerateAccessToken(owl.AuthCode{
|
||||
Code: code,
|
||||
ClientId: "test",
|
||||
RedirectUri: "test",
|
||||
CodeChallenge: "test",
|
||||
CodeChallengeMethod: "test",
|
||||
Scope: "test",
|
||||
})
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("h", "entry")
|
||||
form.Add("content", "Test Content")
|
||||
form.Add("access_token", token)
|
||||
|
||||
req, err := http.NewRequest("POST", user.MicropubUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusCreated)
|
||||
loc_header := rr.Header().Get("Location")
|
||||
assertions.Assert(t, loc_header != "", "Location header should be set")
|
||||
}
|
||||
|
||||
// func TestMicropubAccessTokenInBoth(t *testing.T) {
|
||||
// repo, user := getSingleUserTestRepo()
|
||||
// user.ResetPassword("testpassword")
|
||||
|
||||
// code, _ := user.GenerateAuthCode(
|
||||
// "test", "test", "test", "test", "test",
|
||||
// )
|
||||
// token, _, _ := user.GenerateAccessToken(owl.AuthCode{
|
||||
// Code: code,
|
||||
// ClientId: "test",
|
||||
// RedirectUri: "test",
|
||||
// CodeChallenge: "test",
|
||||
// CodeChallengeMethod: "test",
|
||||
// Scope: "test",
|
||||
// })
|
||||
|
||||
// // Create Request and Response
|
||||
// form := url.Values{}
|
||||
// form.Add("h", "entry")
|
||||
// form.Add("content", "Test Content")
|
||||
// form.Add("access_token", token)
|
||||
|
||||
// req, err := http.NewRequest("POST", user.MicropubUrl(), strings.NewReader(form.Encode()))
|
||||
// req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
// req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
// req.Header.Add("Authorization", "Bearer "+token)
|
||||
// assertions.AssertNoError(t, err, "Error creating request")
|
||||
// rr := httptest.NewRecorder()
|
||||
// router := main.SingleUserRouter(&repo)
|
||||
// router.ServeHTTP(rr, req)
|
||||
|
||||
// assertions.AssertStatus(t, rr, http.StatusBadRequest)
|
||||
// }
|
||||
|
||||
func TestMicropubNoAccessToken(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
code, _ := user.GenerateAuthCode(
|
||||
"test", "test", "test", "test", "test",
|
||||
)
|
||||
user.GenerateAccessToken(owl.AuthCode{
|
||||
Code: code,
|
||||
ClientId: "test",
|
||||
RedirectUri: "test",
|
||||
CodeChallenge: "test",
|
||||
CodeChallengeMethod: "test",
|
||||
Scope: "test",
|
||||
})
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("h", "entry")
|
||||
form.Add("content", "Test Content")
|
||||
|
||||
req, err := http.NewRequest("POST", user.MicropubUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusUnauthorized)
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
package web_test
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
main "h4kor/owl-blogs/cmd/owl/web"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func randomName() string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyz")
|
||||
b := make([]rune, 8)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func testRepoName() string {
|
||||
return "/tmp/" + randomName()
|
||||
}
|
||||
|
||||
func getTestRepo(config owl.RepoConfig) owl.Repository {
|
||||
repo, _ := owl.CreateRepository(testRepoName(), config)
|
||||
return repo
|
||||
}
|
||||
|
||||
func TestMultiUserRepoIndexHandler(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
repo.CreateUser("user_1")
|
||||
repo.CreateUser("user_2")
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
|
||||
// Check the response body contains names of users
|
||||
assertions.AssertContains(t, rr.Body.String(), "user_1")
|
||||
assertions.AssertContains(t, rr.Body.String(), "user_2")
|
||||
}
|
||||
|
||||
func TestMultiUserUserIndexHandler(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", user.UrlPath(), nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
|
||||
// Check the response body contains names of users
|
||||
assertions.AssertContains(t, rr.Body.String(), "post-1")
|
||||
}
|
||||
|
||||
func TestMultiUserPostHandler(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", post.UrlPath(), nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestMultiUserPostMediaHandler(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
|
||||
|
||||
// Create test media file
|
||||
path := path.Join(post.MediaDir(), "data.txt")
|
||||
err := os.WriteFile(path, []byte("test"), 0644)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", post.UrlMediaPath("data.txt"), nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
|
||||
// Check the response body contains data of media file
|
||||
assertions.Assert(t, rr.Body.String() == "test", "Response body is not equal to test")
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package web_test
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
main "h4kor/owl-blogs/cmd/owl/web"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPostHandlerReturns404OnDrafts(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "post-1"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: test\n"
|
||||
content += "draft: true\n"
|
||||
content += "---\n"
|
||||
content += "\n"
|
||||
content += "Write your post here.\n"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", post.UrlPath(), nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusNotFound)
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package web_test
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
main "h4kor/owl-blogs/cmd/owl/web"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMultiUserUserRssIndexHandler(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", user.UrlPath()+"index.xml", nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
|
||||
// Check the response Content-Type is what we expect.
|
||||
assertions.AssertContains(t, rr.Header().Get("Content-Type"), "application/rss+xml")
|
||||
|
||||
// Check the response body contains names of users
|
||||
assertions.AssertContains(t, rr.Body.String(), "post-1")
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
func Router(repo *owl.Repository) http.Handler {
|
||||
router := httprouter.New()
|
||||
router.ServeFiles("/static/*filepath", http.Dir(repo.StaticDir()))
|
||||
router.GET("/", repoIndexHandler(repo))
|
||||
router.GET("/user/:user/", userIndexHandler(repo))
|
||||
router.GET("/user/:user/lists/:list/", postListHandler(repo))
|
||||
// Editor
|
||||
router.GET("/user/:user/editor/auth/", userLoginGetHandler(repo))
|
||||
router.POST("/user/:user/editor/auth/", userLoginPostHandler(repo))
|
||||
router.GET("/user/:user/editor/", userEditorGetHandler(repo))
|
||||
router.POST("/user/:user/editor/", userEditorPostHandler(repo))
|
||||
// Media
|
||||
router.GET("/user/:user/media/*filepath", userMediaHandler(repo))
|
||||
// RSS
|
||||
router.GET("/user/:user/index.xml", userRSSHandler(repo))
|
||||
// Posts
|
||||
router.GET("/user/:user/posts/:post/", postHandler(repo))
|
||||
router.GET("/user/:user/posts/:post/media/*filepath", postMediaHandler(repo))
|
||||
// Webmention
|
||||
router.POST("/user/:user/webmention/", userWebmentionHandler(repo))
|
||||
// Micropub
|
||||
router.POST("/user/:user/micropub/", userMicropubHandler(repo))
|
||||
// IndieAuth
|
||||
router.GET("/user/:user/auth/", userAuthHandler(repo))
|
||||
router.POST("/user/:user/auth/", userAuthProfileHandler(repo))
|
||||
router.POST("/user/:user/auth/verify/", userAuthVerifyHandler(repo))
|
||||
router.POST("/user/:user/auth/token/", userAuthTokenHandler(repo))
|
||||
router.GET("/user/:user/.well-known/oauth-authorization-server", userAuthMetadataHandler(repo))
|
||||
router.NotFound = http.HandlerFunc(notFoundHandler(repo))
|
||||
return router
|
||||
}
|
||||
|
||||
func SingleUserRouter(repo *owl.Repository) http.Handler {
|
||||
router := httprouter.New()
|
||||
router.ServeFiles("/static/*filepath", http.Dir(repo.StaticDir()))
|
||||
router.GET("/", userIndexHandler(repo))
|
||||
router.GET("/lists/:list/", postListHandler(repo))
|
||||
// Editor
|
||||
router.GET("/editor/auth/", userLoginGetHandler(repo))
|
||||
router.POST("/editor/auth/", userLoginPostHandler(repo))
|
||||
router.GET("/editor/", userEditorGetHandler(repo))
|
||||
router.POST("/editor/", userEditorPostHandler(repo))
|
||||
// Media
|
||||
router.GET("/media/*filepath", userMediaHandler(repo))
|
||||
// RSS
|
||||
router.GET("/index.xml", userRSSHandler(repo))
|
||||
// Posts
|
||||
router.GET("/posts/:post/", postHandler(repo))
|
||||
router.GET("/posts/:post/media/*filepath", postMediaHandler(repo))
|
||||
// Webmention
|
||||
router.POST("/webmention/", userWebmentionHandler(repo))
|
||||
// Micropub
|
||||
router.POST("/micropub/", userMicropubHandler(repo))
|
||||
// IndieAuth
|
||||
router.GET("/auth/", userAuthHandler(repo))
|
||||
router.POST("/auth/", userAuthProfileHandler(repo))
|
||||
router.POST("/auth/verify/", userAuthVerifyHandler(repo))
|
||||
router.POST("/auth/token/", userAuthTokenHandler(repo))
|
||||
router.GET("/.well-known/oauth-authorization-server", userAuthMetadataHandler(repo))
|
||||
router.NotFound = http.HandlerFunc(notFoundHandler(repo))
|
||||
return router
|
||||
}
|
||||
|
||||
func StartServer(repoPath string, port int) {
|
||||
var repo owl.Repository
|
||||
var err error
|
||||
repo, err = owl.OpenRepository(repoPath)
|
||||
|
||||
if err != nil {
|
||||
println("Error opening repository: ", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var router http.Handler
|
||||
if config, _ := repo.Config(); config.SingleUser != "" {
|
||||
router = SingleUserRouter(&repo)
|
||||
} else {
|
||||
router = Router(&repo)
|
||||
}
|
||||
|
||||
println("Listening on port", port)
|
||||
http.ListenAndServe(":"+strconv.Itoa(port), router)
|
||||
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
package web_test
|
||||
|
||||
import (
|
||||
owl "h4kor/owl-blogs"
|
||||
main "h4kor/owl-blogs/cmd/owl/web"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func getSingleUserTestRepo() (owl.Repository, owl.User) {
|
||||
repo, _ := owl.CreateRepository(testRepoName(), owl.RepoConfig{SingleUser: "test-1"})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
return repo, user
|
||||
}
|
||||
|
||||
func TestSingleUserUserIndexHandler(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", user.UrlPath(), nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
|
||||
// Check the response body contains names of users
|
||||
assertions.AssertContains(t, rr.Body.String(), "post-1")
|
||||
}
|
||||
|
||||
func TestSingleUserPostHandler(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", post.UrlPath(), nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestSingleUserPostMediaHandler(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
|
||||
|
||||
// Create test media file
|
||||
path := path.Join(post.MediaDir(), "data.txt")
|
||||
err := os.WriteFile(path, []byte("test"), 0644)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", post.UrlMediaPath("data.txt"), nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
|
||||
// Check the response body contains data of media file
|
||||
assertions.Assert(t, rr.Body.String() == "test", "Media file data not returned")
|
||||
}
|
||||
|
||||
func TestHasNoDraftsInList(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
|
||||
content := ""
|
||||
content += "---\n"
|
||||
content += "title: Articles September 2019\n"
|
||||
content += "author: h4kor\n"
|
||||
content += "type: post\n"
|
||||
content += "date: -001-11-30T00:00:00+00:00\n"
|
||||
content += "draft: true\n"
|
||||
content += "url: /?p=426\n"
|
||||
content += "categories:\n"
|
||||
content += " - Uncategorised\n"
|
||||
content += "\n"
|
||||
content += "---\n"
|
||||
content += "<https://nesslabs.com/time-anxiety>\n"
|
||||
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
// Check if title is in the response body
|
||||
assertions.AssertNotContains(t, rr.Body.String(), "Articles September 2019")
|
||||
}
|
||||
|
||||
func TestSingleUserUserPostListHandler(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.CreateNewPost(owl.PostMeta{
|
||||
Title: "post-1",
|
||||
Type: "article",
|
||||
}, "hi")
|
||||
user.CreateNewPost(owl.PostMeta{
|
||||
Title: "post-2",
|
||||
Type: "note",
|
||||
}, "hi")
|
||||
list := owl.PostList{
|
||||
Title: "list-1",
|
||||
Id: "list-1",
|
||||
Include: []string{"article"},
|
||||
}
|
||||
user.AddPostList(list)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("GET", user.ListUrl(list), nil)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusOK)
|
||||
|
||||
// Check the response body contains names of users
|
||||
assertions.AssertContains(t, rr.Body.String(), "post-1")
|
||||
assertions.AssertNotContains(t, rr.Body.String(), "post-2")
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
package web_test
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
main "h4kor/owl-blogs/cmd/owl/web"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setupWebmentionTest(repo owl.Repository, user owl.User, target string, source string) (*httptest.ResponseRecorder, error) {
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("target", target)
|
||||
data.Set("source", source)
|
||||
|
||||
// Create Request and Response
|
||||
req, err := http.NewRequest("POST", user.UrlPath()+"webmention/", strings.NewReader(data.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.Router(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
return rr, nil
|
||||
}
|
||||
|
||||
func TestWebmentionHandleAccepts(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
|
||||
|
||||
target := post.FullUrl()
|
||||
source := "https://example.com"
|
||||
|
||||
rr, err := setupWebmentionTest(repo, user, target, source)
|
||||
assertions.AssertNoError(t, err, "Error setting up webmention test")
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusAccepted)
|
||||
|
||||
}
|
||||
|
||||
func TestWebmentionWrittenToPost(t *testing.T) {
|
||||
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
|
||||
|
||||
target := post.FullUrl()
|
||||
source := "https://example.com"
|
||||
|
||||
rr, err := setupWebmentionTest(repo, user, target, source)
|
||||
assertions.AssertNoError(t, err, "Error setting up webmention test")
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusAccepted)
|
||||
assertions.AssertLen(t, post.IncomingWebmentions(), 1)
|
||||
}
|
||||
|
||||
//
|
||||
// https://www.w3.org/TR/webmention/#h-request-verification
|
||||
//
|
||||
|
||||
// The receiver MUST check that source and target are valid URLs [URL]
|
||||
// and are of schemes that are supported by the receiver.
|
||||
// (Most commonly this means checking that the source and target schemes are http or https).
|
||||
func TestWebmentionSourceValidation(t *testing.T) {
|
||||
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
|
||||
|
||||
target := post.FullUrl()
|
||||
source := "ftp://example.com"
|
||||
|
||||
rr, err := setupWebmentionTest(repo, user, target, source)
|
||||
assertions.AssertNoError(t, err, "Error setting up webmention test")
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestWebmentionTargetValidation(t *testing.T) {
|
||||
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
|
||||
|
||||
target := "ftp://example.com"
|
||||
source := post.FullUrl()
|
||||
|
||||
rr, err := setupWebmentionTest(repo, user, target, source)
|
||||
assertions.AssertNoError(t, err, "Error setting up webmention test")
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// The receiver MUST reject the request if the source URL is the same as the target URL.
|
||||
|
||||
func TestWebmentionSameTargetAndSource(t *testing.T) {
|
||||
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
|
||||
|
||||
target := post.FullUrl()
|
||||
source := post.FullUrl()
|
||||
|
||||
rr, err := setupWebmentionTest(repo, user, target, source)
|
||||
assertions.AssertNoError(t, err, "Error setting up webmention test")
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// The receiver SHOULD check that target is a valid resource for which it can accept Webmentions.
|
||||
// This check SHOULD happen synchronously to reject invalid Webmentions before more in-depth verification begins.
|
||||
// What a "valid resource" means is up to the receiver.
|
||||
func TestValidationOfTarget(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
|
||||
|
||||
target := post.FullUrl()
|
||||
target = target[:len(target)-1] + "invalid"
|
||||
source := post.FullUrl()
|
||||
|
||||
rr, err := setupWebmentionTest(repo, user, target, source)
|
||||
assertions.AssertNoError(t, err, "Error setting up webmention test")
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestAcceptWebmentionForAlias(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("test-1")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - /foo/bar\n"
|
||||
content += " - /foo/baz\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
target := "https://example.com/foo/bar"
|
||||
source := "https://example.com"
|
||||
|
||||
rr, err := setupWebmentionTest(repo, user, target, source)
|
||||
assertions.AssertNoError(t, err, "Error setting up webmention test")
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusAccepted)
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var postId string
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(webmentionCmd)
|
||||
webmentionCmd.Flags().StringVar(
|
||||
&postId, "post", "",
|
||||
"specify the post to send webmentions for. Otherwise, all posts will be checked.",
|
||||
)
|
||||
}
|
||||
|
||||
var webmentionCmd = &cobra.Command{
|
||||
Use: "webmention",
|
||||
Short: "Send webmentions for posts, optionally for a specific user",
|
||||
Long: `Send webmentions for posts, optionally for a specific user`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
repo, err := owl.OpenRepository(repoPath)
|
||||
if err != nil {
|
||||
println("Error opening repository: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var users []owl.User
|
||||
if user == "" {
|
||||
// send webmentions for all users
|
||||
users, err = repo.Users()
|
||||
if err != nil {
|
||||
println("Error getting users: ", err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// send webmentions for a specific user
|
||||
user, err := repo.GetUser(user)
|
||||
users = append(users, user)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
processPost := func(user owl.User, post owl.Post) error {
|
||||
println("Webmentions for post: ", post.Title())
|
||||
|
||||
err := post.ScanForLinks()
|
||||
if err != nil {
|
||||
println("Error scanning post for links: ", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
webmentions := post.OutgoingWebmentions()
|
||||
println("Found ", len(webmentions), " links")
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(webmentions))
|
||||
for _, webmention := range webmentions {
|
||||
go func(webmention owl.WebmentionOut) {
|
||||
defer wg.Done()
|
||||
sendErr := post.SendWebmention(webmention)
|
||||
if sendErr != nil {
|
||||
println("Error sending webmentions: ", sendErr.Error())
|
||||
} else {
|
||||
println("Webmention sent to ", webmention.Target)
|
||||
}
|
||||
}(webmention)
|
||||
}
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
if postId != "" {
|
||||
// send webmentions for a specific post
|
||||
post, err := user.GetPost(postId)
|
||||
if err != nil {
|
||||
println("Error getting post: ", err.Error())
|
||||
return
|
||||
}
|
||||
processPost(user, post)
|
||||
return
|
||||
}
|
||||
|
||||
posts, err := user.PublishedPosts()
|
||||
if err != nil {
|
||||
println("Error getting posts: ", err.Error())
|
||||
}
|
||||
|
||||
for _, post := range posts {
|
||||
processPost(user, post)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package owl
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func dirExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// lists all files/dirs in a directory, not recursive
|
||||
func listDir(path string) []string {
|
||||
dir, _ := os.Open(path)
|
||||
defer dir.Close()
|
||||
files, _ := dir.Readdirnames(-1)
|
||||
return files
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func toDirectoryName(name string) string {
|
||||
name = strings.ToLower(strings.ReplaceAll(name, " ", "-"))
|
||||
// remove all non-alphanumeric characters
|
||||
name = strings.Map(func(r rune) rune {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return r
|
||||
}
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
return r
|
||||
}
|
||||
if r >= '0' && r <= '9' {
|
||||
return r
|
||||
}
|
||||
if r == '-' {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, name)
|
||||
return name
|
||||
}
|
6
embed.go
6
embed.go
|
@ -1,6 +0,0 @@
|
|||
package owl
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed embed/*
|
||||
var embed_files embed.FS
|
|
@ -1,60 +0,0 @@
|
|||
<div class="h-entry">
|
||||
<hgroup>
|
||||
<h1 class="p-name">{{.Title}}</h1>
|
||||
<small>
|
||||
<a class="u-url" href="{{.Post.FullUrl}}">#</a>
|
||||
Published:
|
||||
<time class="dt-published" datetime="{{.Post.Meta.Date}}">
|
||||
{{.Post.Meta.FormattedDate}}
|
||||
</time>
|
||||
{{ if .Post.User.Config.AuthorName }}
|
||||
by
|
||||
<a class="p-author h-card" href="{{.Post.User.FullUrl}}">
|
||||
{{ if .Post.User.AvatarUrl }}
|
||||
<img class="u-photo u-logo" style="height: 1em;" src="{{ .Post.User.AvatarUrl }}"
|
||||
alt="{{ .Post.User.Config.Title }}" />
|
||||
{{ end }}
|
||||
{{.Post.User.Config.AuthorName}}
|
||||
</a>
|
||||
{{ end }}
|
||||
</small>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
{{ if .Post.Meta.Bookmark.Url }}
|
||||
<p style="font-style: italic;filter: opacity(80%);">
|
||||
Bookmark: <a class="u-bookmark-of h-cite" href="{{.Post.Meta.Bookmark.Url}}">
|
||||
{{ if .Post.Meta.Bookmark.Text }}
|
||||
{{.Post.Meta.Bookmark.Text}}
|
||||
{{ else }}
|
||||
{{.Post.Meta.Bookmark.Url}}
|
||||
{{ end }}
|
||||
</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
<div class="e-content">
|
||||
{{.Content}}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
{{if .Post.ApprovedIncomingWebmentions}}
|
||||
<h3>
|
||||
Webmentions
|
||||
</h3>
|
||||
<ul>
|
||||
{{range .Post.ApprovedIncomingWebmentions}}
|
||||
<li>
|
||||
<a href="{{.Source}}">
|
||||
{{if .Title}}
|
||||
{{.Title}}
|
||||
{{else}}
|
||||
{{.Source}}
|
||||
{{end}}
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</div>
|
|
@ -1,24 +0,0 @@
|
|||
<h3>Authorization for {{.ClientId}}</h3>
|
||||
|
||||
<h5>Requesting scope:</h5>
|
||||
<ul>
|
||||
{{range $index, $element := .Scopes}}
|
||||
<li>{{$element}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
<br><br>
|
||||
|
||||
<form action="verify/" method="post">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<input type="hidden" name="client_id" value="{{.ClientId}}">
|
||||
<input type="hidden" name="redirect_uri" value="{{.RedirectUri}}">
|
||||
<input type="hidden" name="response_type" value="{{.ResponseType}}">
|
||||
<input type="hidden" name="state" value="{{.State}}">
|
||||
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
|
||||
<input type="hidden" name="code_challenge" value="{{.CodeChallenge}}">
|
||||
<input type="hidden" name="code_challenge_method" value="{{.CodeChallengeMethod}}">
|
||||
<input type="hidden" name="scope" value="{{.Scope}}">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
|
@ -1,72 +0,0 @@
|
|||
<div class="h-entry">
|
||||
<hgroup>
|
||||
<h1 class="p-name">{{.Title}}</h1>
|
||||
<small>
|
||||
<a class="u-url" href="{{.Post.FullUrl}}">#</a>
|
||||
Published:
|
||||
<time class="dt-published" datetime="{{.Post.Meta.Date}}">
|
||||
{{.Post.Meta.FormattedDate}}
|
||||
</time>
|
||||
{{ if .Post.User.Config.AuthorName }}
|
||||
by
|
||||
<a class="p-author h-card" href="{{.Post.User.FullUrl}}">
|
||||
{{ if .Post.User.AvatarUrl }}
|
||||
<img class="u-photo u-logo" style="height: 1em;" src="{{ .Post.User.AvatarUrl }}"
|
||||
alt="{{ .Post.User.Config.Title }}" />
|
||||
{{ end }}
|
||||
{{.Post.User.Config.AuthorName}}
|
||||
</a>
|
||||
{{ end }}
|
||||
</small>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
{{ if .Post.Meta.Reply.Url }}
|
||||
<p style="font-style: italic;filter: opacity(80%);">
|
||||
In reply to: <a class="u-in-reply-to h-cite" rel="in-reply-to" href="{{.Post.Meta.Reply.Url}}">
|
||||
{{ if .Post.Meta.Reply.Text }}
|
||||
{{.Post.Meta.Reply.Text}}
|
||||
{{ else }}
|
||||
{{.Post.Meta.Reply.Url}}
|
||||
{{ end }}
|
||||
</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{ if .Post.Meta.Bookmark.Url }}
|
||||
<p style="font-style: italic;filter: opacity(80%);">
|
||||
Bookmark: <a class="u-bookmark-of h-cite" href="{{.Post.Meta.Bookmark.Url}}">
|
||||
{{ if .Post.Meta.Bookmark.Text }}
|
||||
{{.Post.Meta.Bookmark.Text}}
|
||||
{{ else }}
|
||||
{{.Post.Meta.Bookmark.Url}}
|
||||
{{ end }}
|
||||
</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
<div class="e-content">
|
||||
{{.Content}}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
{{if .Post.ApprovedIncomingWebmentions}}
|
||||
<h3>
|
||||
Webmentions
|
||||
</h3>
|
||||
<ul>
|
||||
{{range .Post.ApprovedIncomingWebmentions}}
|
||||
<li>
|
||||
<a href="{{.Source}}">
|
||||
{{if .Title}}
|
||||
{{.Title}}
|
||||
{{else}}
|
||||
{{.Source}}
|
||||
{{end}}
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</div>
|
|
@ -1,127 +0,0 @@
|
|||
<details>
|
||||
<summary>Write Article/Page</summary>
|
||||
<form action="" method="post">
|
||||
<h2>Create New Article</h2>
|
||||
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
|
||||
<select name="type">
|
||||
<option value="article">Article</option>
|
||||
<option value="page">Page</option>
|
||||
</select>
|
||||
<label for="title">Title</label>
|
||||
<input type="text" name="title" placeholder="Title" />
|
||||
<label for="description">Description</label>
|
||||
<input type="text" name="description" placeholder="Description" />
|
||||
<label for="content">Content</label>
|
||||
<textarea name="content" placeholder="Content" rows="24"></textarea>
|
||||
<input type="checkbox" name="draft" />
|
||||
<label for="draft">Draft</label>
|
||||
<br><br>
|
||||
<input type="submit" value="Create Article" />
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Upload Photo</summary>
|
||||
<form action="" method="post" enctype="multipart/form-data">
|
||||
<h2>Upload Photo</h2>
|
||||
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
|
||||
<input type="hidden" name="type" value="photo">
|
||||
|
||||
<label for="title">Title</label>
|
||||
<input type="text" name="title" placeholder="Title" />
|
||||
<label for="description">Description</label>
|
||||
<input type="text" name="description" placeholder="Description" />
|
||||
<label for="content">Content</label>
|
||||
<textarea name="content" placeholder="Content" rows="4"></textarea>
|
||||
|
||||
<label for="photo">Photo</label>
|
||||
<input type="file" name="photo" placeholder="Photo" />
|
||||
|
||||
<br><br>
|
||||
<input type="submit" value="Create Article" />
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Write Recipe</summary>
|
||||
<form action="" method="post">
|
||||
<h2>Create new Recipe</h2>
|
||||
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
|
||||
<input type="hidden" name="type" value="recipe">
|
||||
|
||||
<label for="title">Title</label>
|
||||
<input type="text" name="title" placeholder="Title" />
|
||||
|
||||
<label for="yield">Yield</label>
|
||||
<input type="text" name="yield" placeholder="Yield" />
|
||||
|
||||
<label for="duration">Duration</label>
|
||||
<input type="text" name="duration" placeholder="Duration" />
|
||||
|
||||
<label for="description">Description</label>
|
||||
<input type="text" name="description" placeholder="Description" />
|
||||
<label for="ingredients">Ingredients (1 per line)</label>
|
||||
<textarea name="ingredients" placeholder="Ingredients" rows="8"></textarea>
|
||||
|
||||
<label for="content">Instructions</label>
|
||||
<textarea name="content" placeholder="Ingredients" rows="24"></textarea>
|
||||
|
||||
<br><br>
|
||||
<input type="submit" value="Create Reply" />
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Write Note</summary>
|
||||
<form action="" method="post">
|
||||
<h2>Create New Note</h2>
|
||||
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
|
||||
<input type="hidden" name="type" value="note">
|
||||
<label for="content">Content</label>
|
||||
<textarea name="content" placeholder="Content" rows="8"></textarea>
|
||||
<br><br>
|
||||
<input type="submit" value="Create Note" />
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Write Reply</summary>
|
||||
<form action="" method="post">
|
||||
<h2>Create New Reply</h2>
|
||||
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
|
||||
<input type="hidden" name="type" value="reply">
|
||||
|
||||
<label for="reply_url">Reply To</label>
|
||||
<input type="text" name="reply_url" placeholder="URL" />
|
||||
|
||||
<label for="title">Title</label>
|
||||
<input type="text" name="title" placeholder="Title" />
|
||||
|
||||
<label for="content">Content</label>
|
||||
<textarea name="content" placeholder="Content" rows="8"></textarea>
|
||||
|
||||
<br><br>
|
||||
<input type="submit" value="Create Reply" />
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Bookmark</summary>
|
||||
<form action="" method="post">
|
||||
<h2>Create Bookmark</h2>
|
||||
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
|
||||
<input type="hidden" name="type" value="bookmark">
|
||||
|
||||
<label for="bookmark_url">Bookmark</label>
|
||||
<input type="text" name="bookmark_url" placeholder="URL" />
|
||||
|
||||
<label for="title">Title</label>
|
||||
<input type="text" name="title" placeholder="Title" />
|
||||
|
||||
<label for="content">Content</label>
|
||||
<textarea name="content" placeholder="Content" rows="8"></textarea>
|
||||
|
||||
<br><br>
|
||||
<input type="submit" value="Create Bookmark" />
|
||||
</form>
|
||||
</details>
|
|
@ -1,13 +0,0 @@
|
|||
{{ if eq .Error "wrong_password" }}
|
||||
<article style="background-color: #dd867f;color: #481212;padding: 1em;">
|
||||
Wrong Password
|
||||
</article>
|
||||
{{ end }}
|
||||
|
||||
|
||||
<form action="" method="post">
|
||||
<h2>Login to Editor</h2>
|
||||
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
|
||||
<input type="password" name="password" />
|
||||
<input type="submit" value="Login" />
|
||||
</form>
|
|
@ -1,4 +0,0 @@
|
|||
<article style="background-color: #dd867f;color: #481212;">
|
||||
<h3>{{ .Error }}</h3>
|
||||
{{ .Message }}
|
||||
</article>
|
|
@ -1,146 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ .Title }} - {{ .User.Config.Title }}</title>
|
||||
|
||||
{{ if .User.FaviconUrl }}
|
||||
<link rel="icon" href="{{ .User.FaviconUrl }}">
|
||||
{{ else }}
|
||||
<link rel="icon" href="data:,">
|
||||
{{ end }}
|
||||
|
||||
<meta property="og:title" content="{{ .Title }}" />
|
||||
{{ if .Description }}
|
||||
<meta name="description" content="{{ .Description }}">
|
||||
<meta property="og:description" content="{{ .Description }}" />
|
||||
{{ end }}
|
||||
{{ if .Type }}
|
||||
<meta property="og:type" content="{{ .Type }}" />
|
||||
{{ end }}
|
||||
{{ if .SelfUrl }}
|
||||
<meta property="og:url" content="{{ .SelfUrl }}" />
|
||||
{{ end }}
|
||||
|
||||
<link rel="stylesheet" href="/static/pico.min.css">
|
||||
<link rel="webmention" href="{{ .User.WebmentionUrl }}">
|
||||
{{ if .User.AuthUrl }}
|
||||
<link rel="indieauth-metadata" href="{{ .User.IndieauthMetadataUrl }}">
|
||||
<link rel="authorization_endpoint" href="{{ .User.AuthUrl}}">
|
||||
<link rel="token_endpoint" href="{{ .User.TokenUrl}}">
|
||||
<link rel="micropub" href="{{ .User.MicropubUrl}}">
|
||||
{{ end }}
|
||||
<style>
|
||||
header {
|
||||
background-color: {{.User.Config.HeaderColor}};
|
||||
padding-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
footer {
|
||||
border-top: dashed 2px;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
float: left;
|
||||
margin-right: 1rem;
|
||||
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
order: 0;
|
||||
}
|
||||
|
||||
.header-profile {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
hgroup h2 a { color: inherit; }
|
||||
|
||||
.photo-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.photo-grid-item {
|
||||
flex: 1 0 25%;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.photo-grid-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 1 / 1 ;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div class="container header h-card">
|
||||
<hgroup class="header-title">
|
||||
<h2><a class="p-name u-url" href="{{ .User.UrlPath }}">{{ .User.Config.Title }}</a></h2>
|
||||
<h3 class="p-note">{{ .User.Config.SubTitle }}</h3>
|
||||
</hgroup>
|
||||
|
||||
<div class="header-profile">
|
||||
{{ if .User.AvatarUrl }}
|
||||
<img class="u-photo u-logo avatar" src="{{ .User.AvatarUrl }}" alt="{{ .User.Config.Title }}" width="100" height="100" />
|
||||
{{ end }}
|
||||
<div style="float: right; list-style: none;">
|
||||
{{ range $me := .User.Config.Me }}
|
||||
<li><a href="{{$me.Url}}" rel="me">{{$me.Name}}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
{{ range $link := .User.Config.HeaderMenu }}
|
||||
{{ if $link.List }}
|
||||
<li><a href="{{ listUrl $.User $link.List }}">{{ $link.Title }}</a></li>
|
||||
{{ else if $link.Post }}
|
||||
<li><a href="{{ postUrl $.User $link.Post }}">{{ $link.Title }}</a></li>
|
||||
{{ else }}
|
||||
<li><a href="{{ $link.Url }}">{{ $link.Title }}</a></li>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
{{ .Content }}
|
||||
</main>
|
||||
<footer class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
{{ range $link := .User.Config.FooterMenu }}
|
||||
{{ if $link.List }}
|
||||
<li><a href="{{ listUrl $.User $link.List }}">{{ $link.Title }}</a></li>
|
||||
{{ else if $link.Post }}
|
||||
<li><a href="{{ postUrl $.User $link.Post }}">{{ $link.Title }}</a></li>
|
||||
{{ else }}
|
||||
<li><a href="{{ $link.Url }}">{{ $link.Title }}</a></li>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</ul>
|
||||
</nav>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,5 +0,0 @@
|
|||
<ul>
|
||||
{{ range .UserLinks }}
|
||||
<li><a href="{{.Href}}">{{.Text}}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
|
@ -1,16 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ .Title }}</title>
|
||||
<link rel="stylesheet" href="/static/pico.min.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{ .Content }}
|
||||
</body>
|
||||
|
||||
</html>
|
File diff suppressed because one or more lines are too long
|
@ -1,47 +0,0 @@
|
|||
<div class="h-entry">
|
||||
<hgroup>
|
||||
<small>
|
||||
<a class="u-url" href="{{.Post.FullUrl}}">#</a>
|
||||
Published:
|
||||
<time class="dt-published" datetime="{{.Post.Meta.Date}}">
|
||||
{{.Post.Meta.FormattedDate}}
|
||||
</time>
|
||||
{{ if .Post.User.Config.AuthorName }}
|
||||
by
|
||||
<a class="p-author h-card" href="{{.Post.User.FullUrl}}">
|
||||
{{ if .Post.User.AvatarUrl }}
|
||||
<img class="u-photo u-logo" style="height: 1em;" src="{{ .Post.User.AvatarUrl }}"
|
||||
alt="{{ .Post.User.Config.Title }}" />
|
||||
{{ end }}
|
||||
{{.Post.User.Config.AuthorName}}
|
||||
</a>
|
||||
{{ end }}
|
||||
</small>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
<div class="e-content">
|
||||
{{.Content}}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
{{if .Post.ApprovedIncomingWebmentions}}
|
||||
<h3>
|
||||
Webmentions
|
||||
</h3>
|
||||
<ul>
|
||||
{{range .Post.ApprovedIncomingWebmentions}}
|
||||
<li>
|
||||
<a href="{{.Source}}">
|
||||
{{if .Title}}
|
||||
{{.Title}}
|
||||
{{else}}
|
||||
{{.Source}}
|
||||
{{end}}
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</div>
|
|
@ -1,34 +0,0 @@
|
|||
<div class="h-entry">
|
||||
<hgroup>
|
||||
<h1 class="p-name">{{.Title}}</h1>
|
||||
<small>
|
||||
<a class="u-url" href="{{.Post.FullUrl}}">#</a>
|
||||
</small>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
<div class="e-content">
|
||||
{{.Content}}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
{{if .Post.ApprovedIncomingWebmentions}}
|
||||
<h3>
|
||||
Webmentions
|
||||
</h3>
|
||||
<ul>
|
||||
{{range .Post.ApprovedIncomingWebmentions}}
|
||||
<li>
|
||||
<a href="{{.Source}}">
|
||||
{{if .Title}}
|
||||
{{.Title}}
|
||||
{{else}}
|
||||
{{.Source}}
|
||||
{{end}}
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</div>
|
|
@ -1,52 +0,0 @@
|
|||
<div class="h-entry">
|
||||
<hgroup>
|
||||
<h1 class="p-name">{{.Title}}</h1>
|
||||
<small>
|
||||
<a class="u-url" href="{{.Post.FullUrl}}">#</a>
|
||||
Published:
|
||||
<time class="dt-published" datetime="{{.Post.Meta.Date}}">
|
||||
{{.Post.Meta.FormattedDate}}
|
||||
</time>
|
||||
{{ if .Post.User.Config.AuthorName }}
|
||||
by
|
||||
<a class="p-author h-card" href="{{.Post.User.FullUrl}}">
|
||||
{{ if .Post.User.AvatarUrl }}
|
||||
<img class="u-photo u-logo" style="height: 1em;" src="{{ .Post.User.AvatarUrl }}"
|
||||
alt="{{ .Post.User.Config.Title }}" />
|
||||
{{ end }}
|
||||
{{.Post.User.Config.AuthorName}}
|
||||
</a>
|
||||
{{ end }}
|
||||
</small>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
{{ if .Post.Meta.PhotoPath }}
|
||||
<img class="u-photo" src="media/{{.Post.Meta.PhotoPath}}" alt="{{.Post.Meta.Description}}" />
|
||||
{{ end }}
|
||||
|
||||
<div class="e-content">
|
||||
{{.Content}}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
{{if .Post.ApprovedIncomingWebmentions}}
|
||||
<h3>
|
||||
Webmentions
|
||||
</h3>
|
||||
<ul>
|
||||
{{range .Post.ApprovedIncomingWebmentions}}
|
||||
<li>
|
||||
<a href="{{.Source}}">
|
||||
{{if .Title}}
|
||||
{{.Title}}
|
||||
{{else}}
|
||||
{{.Source}}
|
||||
{{end}}
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</div>
|
|
@ -1,9 +0,0 @@
|
|||
<div class="h-feed photo-grid">
|
||||
{{range .}}
|
||||
<div class="h-entry photo-grid-item">
|
||||
<a class="u-url" href="{{.UrlPath}}">
|
||||
<img class="u-photo" src="{{.UrlPath}}media/{{.Meta.PhotoPath}}" alt="{{.Meta.Description}}" />
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
|
@ -1,25 +0,0 @@
|
|||
<div class="h-feed">
|
||||
{{range .}}
|
||||
<div class="h-entry">
|
||||
<hgroup>
|
||||
{{ if eq .Meta.Type "note"}}
|
||||
<h6><a class="u-url" href="{{.UrlPath}}">
|
||||
{{ if .Title }}{{.Title}}{{ else }}#{{.Id}}{{ end }}
|
||||
</a></h6>
|
||||
<p>{{.RenderedContent | noescape}}</p>
|
||||
{{ else }}
|
||||
<h3><a class="u-url" href="{{.UrlPath}}">
|
||||
{{ if .Title }}{{.Title}}{{ else }}#{{.Id}}{{ end }}
|
||||
</a></h3>
|
||||
{{ end }}
|
||||
<small style="font-size: 0.75em;">
|
||||
Published:
|
||||
<time class="dt-published" datetime="{{.Meta.Date}}">
|
||||
{{.Meta.FormattedDate}}
|
||||
</time>
|
||||
</small>
|
||||
</hgroup>
|
||||
</div>
|
||||
<hr>
|
||||
{{end}}
|
||||
</div>
|
|
@ -1,71 +0,0 @@
|
|||
<div class="h-entry">
|
||||
<hgroup>
|
||||
<h1 class="p-name">{{.Title}}</h1>
|
||||
<small>
|
||||
<a class="u-url" href="{{.Post.FullUrl}}">#</a>
|
||||
Published:
|
||||
<time class="dt-published" datetime="{{.Post.Meta.Date}}">
|
||||
{{.Post.Meta.FormattedDate}}
|
||||
</time>
|
||||
{{ if .Post.User.Config.AuthorName }}
|
||||
by
|
||||
<a class="p-author h-card" href="{{.Post.User.FullUrl}}">
|
||||
{{ if .Post.User.AvatarUrl }}
|
||||
<img class="u-photo u-logo" style="height: 1em;" src="{{ .Post.User.AvatarUrl }}" alt="{{ .Post.User.Config.Title }}" />
|
||||
{{ end }}
|
||||
{{.Post.User.Config.AuthorName}}
|
||||
</a>
|
||||
{{ end }}
|
||||
</small>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
{{ if .Post.Meta.Reply.Url }}
|
||||
<p style="font-style: italic;filter: opacity(80%);">
|
||||
In reply to: <a class="u-in-reply-to h-cite" rel="in-reply-to" href="{{.Post.Meta.Reply.Url}}">
|
||||
{{ if .Post.Meta.Reply.Text }}
|
||||
{{.Post.Meta.Reply.Text}}
|
||||
{{ else }}
|
||||
{{.Post.Meta.Reply.Url}}
|
||||
{{ end }}
|
||||
</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{ if .Post.Meta.Bookmark.Url }}
|
||||
<p style="font-style: italic;filter: opacity(80%);">
|
||||
Bookmark: <a class="u-bookmark-of h-cite" href="{{.Post.Meta.Bookmark.Url}}">
|
||||
{{ if .Post.Meta.Bookmark.Text }}
|
||||
{{.Post.Meta.Bookmark.Text}}
|
||||
{{ else }}
|
||||
{{.Post.Meta.Bookmark.Url}}
|
||||
{{ end }}
|
||||
</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
<div class="e-content">
|
||||
{{.Content}}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
{{if .Post.ApprovedIncomingWebmentions}}
|
||||
<h3>
|
||||
Webmentions
|
||||
</h3>
|
||||
<ul>
|
||||
{{range .Post.ApprovedIncomingWebmentions}}
|
||||
<li>
|
||||
<a href="{{.Source}}">
|
||||
{{if .Title}}
|
||||
{{.Title}}
|
||||
{{else}}
|
||||
{{.Source}}
|
||||
{{end}}
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</div>
|
|
@ -1,78 +0,0 @@
|
|||
<div class="h-entry h-recipe">
|
||||
<hgroup>
|
||||
<h1 class="p-name">{{.Title}}</h1>
|
||||
<small>
|
||||
<a class="u-url" href="{{.Post.FullUrl}}">#</a>
|
||||
Published:
|
||||
<time class="dt-published" datetime="{{.Post.Meta.Date}}">
|
||||
{{.Post.Meta.FormattedDate}}
|
||||
</time>
|
||||
{{ if .Post.User.Config.AuthorName }}
|
||||
by
|
||||
<a class="p-author h-card" href="{{.Post.User.FullUrl}}">
|
||||
{{ if .Post.User.AvatarUrl }}
|
||||
<img class="u-photo u-logo" style="height: 1em;" src="{{ .Post.User.AvatarUrl }}"
|
||||
alt="{{ .Post.User.Config.Title }}" />
|
||||
{{ end }}
|
||||
{{.Post.User.Config.AuthorName}}
|
||||
</a>
|
||||
{{ end }}
|
||||
</small>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
|
||||
<div class="e-content">
|
||||
<small>
|
||||
|
||||
{{ if .Post.Meta.Recipe.Yield }}
|
||||
Servings: <span class="p-yield">{{ .Post.Meta.Recipe.Yield }}</span>
|
||||
{{ if .Post.Meta.Recipe.Duration }}, {{end}}
|
||||
|
||||
{{ end }}
|
||||
|
||||
{{ if .Post.Meta.Recipe.Duration }}
|
||||
Prep Time: <time class="dt-duration" value="{{ .Post.Meta.Recipe.Duration }}">
|
||||
{{ .Post.Meta.Recipe.Duration }}
|
||||
</time>
|
||||
{{ end }}
|
||||
</small>
|
||||
|
||||
<h2>Ingredients</h2>
|
||||
|
||||
<ul>
|
||||
{{ range $ingredient := .Post.Meta.Recipe.Ingredients }}
|
||||
<li class="p-ingredient">
|
||||
{{ $ingredient }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
||||
<h2>Instructions</h2>
|
||||
|
||||
<div class="e-instructions">
|
||||
{{.Content}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
{{if .Post.ApprovedIncomingWebmentions}}
|
||||
<h3>
|
||||
Webmentions
|
||||
</h3>
|
||||
<ul>
|
||||
{{range .Post.ApprovedIncomingWebmentions}}
|
||||
<li>
|
||||
<a href="{{.Source}}">
|
||||
{{if .Title}}
|
||||
{{.Title}}
|
||||
{{else}}
|
||||
{{.Source}}
|
||||
{{end}}
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</div>
|
|
@ -1,60 +0,0 @@
|
|||
<div class="h-entry">
|
||||
<hgroup>
|
||||
<h1 class="p-name">{{.Title}}</h1>
|
||||
<small>
|
||||
<a class="u-url" href="{{.Post.FullUrl}}">#</a>
|
||||
Published:
|
||||
<time class="dt-published" datetime="{{.Post.Meta.Date}}">
|
||||
{{.Post.Meta.FormattedDate}}
|
||||
</time>
|
||||
{{ if .Post.User.Config.AuthorName }}
|
||||
by
|
||||
<a class="p-author h-card" href="{{.Post.User.FullUrl}}">
|
||||
{{ if .Post.User.AvatarUrl }}
|
||||
<img class="u-photo u-logo" style="height: 1em;" src="{{ .Post.User.AvatarUrl }}"
|
||||
alt="{{ .Post.User.Config.Title }}" />
|
||||
{{ end }}
|
||||
{{.Post.User.Config.AuthorName}}
|
||||
</a>
|
||||
{{ end }}
|
||||
</small>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
{{ if .Post.Meta.Reply.Url }}
|
||||
<p style="font-style: italic;filter: opacity(80%);">
|
||||
In reply to: <a class="u-in-reply-to h-cite" rel="in-reply-to" href="{{.Post.Meta.Reply.Url}}">
|
||||
{{ if .Post.Meta.Reply.Text }}
|
||||
{{.Post.Meta.Reply.Text}}
|
||||
{{ else }}
|
||||
{{.Post.Meta.Reply.Url}}
|
||||
{{ end }}
|
||||
</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
<div class="e-content">
|
||||
{{.Content}}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
{{if .Post.ApprovedIncomingWebmentions}}
|
||||
<h3>
|
||||
Webmentions
|
||||
</h3>
|
||||
<ul>
|
||||
{{range .Post.ApprovedIncomingWebmentions}}
|
||||
<li>
|
||||
<a href="{{.Source}}">
|
||||
{{if .Title}}
|
||||
{{.Title}}
|
||||
{{else}}
|
||||
{{.Source}}
|
||||
{{end}}
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</div>
|
|
@ -1,72 +0,0 @@
|
|||
<div class="h-entry">
|
||||
<hgroup>
|
||||
<h1 class="p-name">{{.Title}}</h1>
|
||||
<small>
|
||||
<a class="u-url" href="{{.Post.FullUrl}}">#</a>
|
||||
Published:
|
||||
<time class="dt-published" datetime="{{.Post.Meta.Date}}">
|
||||
{{.Post.Meta.FormattedDate}}
|
||||
</time>
|
||||
{{ if .Post.User.Config.AuthorName }}
|
||||
by
|
||||
<a class="p-author h-card" href="{{.Post.User.FullUrl}}">
|
||||
{{ if .Post.User.AvatarUrl }}
|
||||
<img class="u-photo u-logo" style="height: 1em;" src="{{ .Post.User.AvatarUrl }}"
|
||||
alt="{{ .Post.User.Config.Title }}" />
|
||||
{{ end }}
|
||||
{{.Post.User.Config.AuthorName}}
|
||||
</a>
|
||||
{{ end }}
|
||||
</small>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
{{ if .Post.Meta.Reply.Url }}
|
||||
<p style="font-style: italic;filter: opacity(80%);">
|
||||
In reply to: <a class="u-in-reply-to h-cite" rel="in-reply-to" href="{{.Post.Meta.Reply.Url}}">
|
||||
{{ if .Post.Meta.Reply.Text }}
|
||||
{{.Post.Meta.Reply.Text}}
|
||||
{{ else }}
|
||||
{{.Post.Meta.Reply.Url}}
|
||||
{{ end }}
|
||||
</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{ if .Post.Meta.Bookmark.Url }}
|
||||
<p style="font-style: italic;filter: opacity(80%);">
|
||||
Bookmark: <a class="u-bookmark-of h-cite" href="{{.Post.Meta.Bookmark.Url}}">
|
||||
{{ if .Post.Meta.Bookmark.Text }}
|
||||
{{.Post.Meta.Bookmark.Text}}
|
||||
{{ else }}
|
||||
{{.Post.Meta.Bookmark.Url}}
|
||||
{{ end }}
|
||||
</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
<div class="e-content">
|
||||
{{.Content}}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
{{if .Post.ApprovedIncomingWebmentions}}
|
||||
<h3>
|
||||
Webmentions
|
||||
</h3>
|
||||
<ul>
|
||||
{{range .Post.ApprovedIncomingWebmentions}}
|
||||
<li>
|
||||
<a href="{{.Source}}">
|
||||
{{if .Title}}
|
||||
{{.Title}}
|
||||
{{else}}
|
||||
{{.Source}}
|
||||
{{end}}
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</div>
|
|
@ -1,9 +0,0 @@
|
|||
{{range .}}
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ .UrlPath }}">
|
||||
{{ .Name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{end}}
|
23
files.go
23
files.go
|
@ -1,23 +0,0 @@
|
|||
package owl
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func saveToYaml(path string, data interface{}) error {
|
||||
bytes, err := yaml.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, bytes, 0644)
|
||||
}
|
||||
|
||||
func loadFromYaml(path string, data interface{}) error {
|
||||
bytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return yaml.Unmarshal(bytes, data)
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 2.9 KiB |
17
go.mod
17
go.mod
|
@ -1,17 +0,0 @@
|
|||
module h4kor/owl-blogs
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/yuin/goldmark v1.4.13
|
||||
golang.org/x/net v0.1.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/crypto v0.1.0 // indirect
|
||||
)
|
23
go.sum
23
go.sum
|
@ -1,23 +0,0 @@
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
|
||||
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
|
||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
269
html.go
269
html.go
|
@ -1,269 +0,0 @@
|
|||
package owl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
type HtmlParser interface {
|
||||
ParseHEntry(resp *http.Response) (ParsedHEntry, error)
|
||||
ParseLinks(resp *http.Response) ([]string, error)
|
||||
ParseLinksFromString(string) ([]string, error)
|
||||
GetWebmentionEndpoint(resp *http.Response) (string, error)
|
||||
GetRedirctUris(resp *http.Response) ([]string, error)
|
||||
}
|
||||
|
||||
type OwlHtmlParser struct{}
|
||||
|
||||
type ParsedHEntry struct {
|
||||
Title string
|
||||
}
|
||||
|
||||
func collectText(n *html.Node, buf *bytes.Buffer) {
|
||||
|
||||
if n.Type == html.TextNode {
|
||||
buf.WriteString(n.Data)
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
collectText(c, buf)
|
||||
}
|
||||
}
|
||||
|
||||
func readResponseBody(resp *http.Response) (string, error) {
|
||||
defer resp.Body.Close()
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bodyBytes), nil
|
||||
}
|
||||
|
||||
func (OwlHtmlParser) ParseHEntry(resp *http.Response) (ParsedHEntry, error) {
|
||||
htmlStr, err := readResponseBody(resp)
|
||||
if err != nil {
|
||||
return ParsedHEntry{}, err
|
||||
}
|
||||
doc, err := html.Parse(strings.NewReader(htmlStr))
|
||||
if err != nil {
|
||||
return ParsedHEntry{}, err
|
||||
}
|
||||
|
||||
var interpretHFeed func(*html.Node, *ParsedHEntry, bool) (ParsedHEntry, error)
|
||||
interpretHFeed = func(n *html.Node, curr *ParsedHEntry, parent bool) (ParsedHEntry, error) {
|
||||
attrs := n.Attr
|
||||
for _, attr := range attrs {
|
||||
if attr.Key == "class" && strings.Contains(attr.Val, "p-name") {
|
||||
buf := &bytes.Buffer{}
|
||||
collectText(n, buf)
|
||||
curr.Title = buf.String()
|
||||
return *curr, nil
|
||||
}
|
||||
}
|
||||
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
interpretHFeed(c, curr, false)
|
||||
}
|
||||
return *curr, nil
|
||||
}
|
||||
|
||||
var findHFeed func(*html.Node) (ParsedHEntry, error)
|
||||
findHFeed = func(n *html.Node) (ParsedHEntry, error) {
|
||||
attrs := n.Attr
|
||||
for _, attr := range attrs {
|
||||
if attr.Key == "class" && strings.Contains(attr.Val, "h-entry") {
|
||||
return interpretHFeed(n, &ParsedHEntry{}, true)
|
||||
}
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
entry, err := findHFeed(c)
|
||||
if err == nil {
|
||||
return entry, nil
|
||||
}
|
||||
}
|
||||
return ParsedHEntry{}, errors.New("no h-entry found")
|
||||
}
|
||||
return findHFeed(doc)
|
||||
}
|
||||
|
||||
func (OwlHtmlParser) ParseLinks(resp *http.Response) ([]string, error) {
|
||||
htmlStr, err := readResponseBody(resp)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
return OwlHtmlParser{}.ParseLinksFromString(htmlStr)
|
||||
}
|
||||
|
||||
func (OwlHtmlParser) ParseLinksFromString(htmlStr string) ([]string, error) {
|
||||
doc, err := html.Parse(strings.NewReader(htmlStr))
|
||||
if err != nil {
|
||||
return make([]string, 0), err
|
||||
}
|
||||
|
||||
var findLinks func(*html.Node) ([]string, error)
|
||||
findLinks = func(n *html.Node) ([]string, error) {
|
||||
links := make([]string, 0)
|
||||
if n.Type == html.ElementNode && n.Data == "a" {
|
||||
for _, attr := range n.Attr {
|
||||
if attr.Key == "href" {
|
||||
links = append(links, attr.Val)
|
||||
}
|
||||
}
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
childLinks, _ := findLinks(c)
|
||||
links = append(links, childLinks...)
|
||||
}
|
||||
return links, nil
|
||||
}
|
||||
return findLinks(doc)
|
||||
}
|
||||
|
||||
func (OwlHtmlParser) GetWebmentionEndpoint(resp *http.Response) (string, error) {
|
||||
//request url
|
||||
requestUrl := resp.Request.URL
|
||||
|
||||
// Check link headers
|
||||
for _, linkHeader := range resp.Header["Link"] {
|
||||
linkHeaderParts := strings.Split(linkHeader, ",")
|
||||
for _, linkHeaderPart := range linkHeaderParts {
|
||||
linkHeaderPart = strings.TrimSpace(linkHeaderPart)
|
||||
params := strings.Split(linkHeaderPart, ";")
|
||||
if len(params) != 2 {
|
||||
continue
|
||||
}
|
||||
for _, param := range params[1:] {
|
||||
param = strings.TrimSpace(param)
|
||||
if strings.Contains(param, "webmention") {
|
||||
link := strings.Split(params[0], ";")[0]
|
||||
link = strings.Trim(link, "<>")
|
||||
linkUrl, err := url.Parse(link)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return requestUrl.ResolveReference(linkUrl).String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
htmlStr, err := readResponseBody(resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
doc, err := html.Parse(strings.NewReader(htmlStr))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var findEndpoint func(*html.Node) (string, error)
|
||||
findEndpoint = func(n *html.Node) (string, error) {
|
||||
if n.Type == html.ElementNode && (n.Data == "link" || n.Data == "a") {
|
||||
for _, attr := range n.Attr {
|
||||
if attr.Key == "rel" {
|
||||
vals := strings.Split(attr.Val, " ")
|
||||
for _, val := range vals {
|
||||
if val == "webmention" {
|
||||
for _, attr := range n.Attr {
|
||||
if attr.Key == "href" {
|
||||
return attr.Val, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
endpoint, err := findEndpoint(c)
|
||||
if err == nil {
|
||||
return endpoint, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("no webmention endpoint found")
|
||||
}
|
||||
linkUrlStr, err := findEndpoint(doc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
linkUrl, err := url.Parse(linkUrlStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return requestUrl.ResolveReference(linkUrl).String(), nil
|
||||
}
|
||||
|
||||
func (OwlHtmlParser) GetRedirctUris(resp *http.Response) ([]string, error) {
|
||||
//request url
|
||||
requestUrl := resp.Request.URL
|
||||
|
||||
htmlStr, err := readResponseBody(resp)
|
||||
if err != nil {
|
||||
return make([]string, 0), err
|
||||
}
|
||||
doc, err := html.Parse(strings.NewReader(htmlStr))
|
||||
if err != nil {
|
||||
return make([]string, 0), err
|
||||
}
|
||||
|
||||
var findLinks func(*html.Node) ([]string, error)
|
||||
// Check link headers
|
||||
header_links := make([]string, 0)
|
||||
for _, linkHeader := range resp.Header["Link"] {
|
||||
linkHeaderParts := strings.Split(linkHeader, ",")
|
||||
for _, linkHeaderPart := range linkHeaderParts {
|
||||
linkHeaderPart = strings.TrimSpace(linkHeaderPart)
|
||||
params := strings.Split(linkHeaderPart, ";")
|
||||
if len(params) != 2 {
|
||||
continue
|
||||
}
|
||||
for _, param := range params[1:] {
|
||||
param = strings.TrimSpace(param)
|
||||
if strings.Contains(param, "redirect_uri") {
|
||||
link := strings.Split(params[0], ";")[0]
|
||||
link = strings.Trim(link, "<>")
|
||||
linkUrl, err := url.Parse(link)
|
||||
if err == nil {
|
||||
header_links = append(header_links, requestUrl.ResolveReference(linkUrl).String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findLinks = func(n *html.Node) ([]string, error) {
|
||||
links := make([]string, 0)
|
||||
if n.Type == html.ElementNode && n.Data == "link" {
|
||||
// check for rel="redirect_uri"
|
||||
rel := ""
|
||||
href := ""
|
||||
|
||||
for _, attr := range n.Attr {
|
||||
if attr.Key == "href" {
|
||||
href = attr.Val
|
||||
}
|
||||
if attr.Key == "rel" {
|
||||
rel = attr.Val
|
||||
}
|
||||
}
|
||||
if rel == "redirect_uri" {
|
||||
linkUrl, err := url.Parse(href)
|
||||
if err == nil {
|
||||
links = append(links, requestUrl.ResolveReference(linkUrl).String())
|
||||
}
|
||||
}
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
childLinks, _ := findLinks(c)
|
||||
links = append(links, childLinks...)
|
||||
}
|
||||
return links, nil
|
||||
}
|
||||
body_links, err := findLinks(doc)
|
||||
return append(body_links, header_links...), err
|
||||
}
|
15
http.go
15
http.go
|
@ -1,15 +0,0 @@
|
|||
package owl
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type HttpClient interface {
|
||||
Get(url string) (resp *http.Response, err error)
|
||||
Post(url, contentType string, body io.Reader) (resp *http.Response, err error)
|
||||
PostForm(url string, data url.Values) (resp *http.Response, err error)
|
||||
}
|
||||
|
||||
type OwlHttpClient = http.Client
|
45
owl_test.go
45
owl_test.go
|
@ -1,45 +0,0 @@
|
|||
package owl_test
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
func randomName() string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyz")
|
||||
b := make([]rune, 8)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func testRepoName() string {
|
||||
return "/tmp/" + randomName()
|
||||
}
|
||||
|
||||
func randomUserName() string {
|
||||
return randomName()
|
||||
}
|
||||
|
||||
func getTestUser() owl.User {
|
||||
repo, _ := owl.CreateRepository(testRepoName(), owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
return user
|
||||
}
|
||||
|
||||
func getTestRepo(config owl.RepoConfig) owl.Repository {
|
||||
repo, _ := owl.CreateRepository(testRepoName(), config)
|
||||
return repo
|
||||
}
|
||||
|
||||
func contains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
478
post.go
478
post.go
|
@ -1,478 +0,0 @@
|
|||
package owl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type GenericPost struct {
|
||||
user *User
|
||||
id string
|
||||
metaLoaded bool
|
||||
meta PostMeta
|
||||
wmLock sync.Mutex
|
||||
}
|
||||
|
||||
func (post *GenericPost) TemplateDir() string {
|
||||
return post.Meta().Type
|
||||
}
|
||||
|
||||
type Post interface {
|
||||
TemplateDir() string
|
||||
|
||||
// Actual Data
|
||||
User() *User
|
||||
Id() string
|
||||
Title() string
|
||||
Meta() PostMeta
|
||||
Content() []byte
|
||||
RenderedContent() string
|
||||
Aliases() []string
|
||||
|
||||
// Filesystem
|
||||
Dir() string
|
||||
MediaDir() string
|
||||
ContentFile() string
|
||||
|
||||
// Urls
|
||||
UrlPath() string
|
||||
FullUrl() string
|
||||
UrlMediaPath(filename string) string
|
||||
|
||||
// Webmentions Support
|
||||
IncomingWebmentions() []WebmentionIn
|
||||
OutgoingWebmentions() []WebmentionOut
|
||||
PersistIncomingWebmention(webmention WebmentionIn) error
|
||||
PersistOutgoingWebmention(webmention *WebmentionOut) error
|
||||
AddIncomingWebmention(source string) error
|
||||
EnrichWebmention(webmention WebmentionIn) error
|
||||
ApprovedIncomingWebmentions() []WebmentionIn
|
||||
ScanForLinks() error
|
||||
SendWebmention(webmention WebmentionOut) error
|
||||
}
|
||||
|
||||
type ReplyData struct {
|
||||
Url string `yaml:"url"`
|
||||
Text string `yaml:"text"`
|
||||
}
|
||||
type BookmarkData struct {
|
||||
Url string `yaml:"url"`
|
||||
Text string `yaml:"text"`
|
||||
}
|
||||
|
||||
type RecipeData struct {
|
||||
Yield string `yaml:"yield"`
|
||||
Duration string `yaml:"duration"`
|
||||
Ingredients []string `yaml:"ingredients"`
|
||||
}
|
||||
|
||||
type PostMeta struct {
|
||||
Type string `yaml:"type"`
|
||||
Title string `yaml:"title"`
|
||||
Description string `yaml:"description"`
|
||||
Aliases []string `yaml:"aliases"`
|
||||
Date time.Time `yaml:"date"`
|
||||
Draft bool `yaml:"draft"`
|
||||
Reply ReplyData `yaml:"reply"`
|
||||
Bookmark BookmarkData `yaml:"bookmark"`
|
||||
Recipe RecipeData `yaml:"recipe"`
|
||||
PhotoPath string `yaml:"photo"`
|
||||
}
|
||||
|
||||
func (pm PostMeta) FormattedDate() string {
|
||||
return pm.Date.Format("02-01-2006 15:04:05")
|
||||
}
|
||||
|
||||
func (pm *PostMeta) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type T struct {
|
||||
Type string `yaml:"type"`
|
||||
Title string `yaml:"title"`
|
||||
Description string `yaml:"description"`
|
||||
Aliases []string `yaml:"aliases"`
|
||||
Draft bool `yaml:"draft"`
|
||||
Reply ReplyData `yaml:"reply"`
|
||||
Bookmark BookmarkData `yaml:"bookmark"`
|
||||
Recipe RecipeData `yaml:"recipe"`
|
||||
PhotoPath string `yaml:"photo"`
|
||||
}
|
||||
type S struct {
|
||||
Date string `yaml:"date"`
|
||||
}
|
||||
|
||||
var t T
|
||||
var s S
|
||||
if err := unmarshal(&t); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := unmarshal(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pm.Type = t.Type
|
||||
if pm.Type == "" {
|
||||
pm.Type = "article"
|
||||
}
|
||||
pm.Title = t.Title
|
||||
pm.Description = t.Description
|
||||
pm.Aliases = t.Aliases
|
||||
pm.Draft = t.Draft
|
||||
pm.Reply = t.Reply
|
||||
pm.Bookmark = t.Bookmark
|
||||
pm.Recipe = t.Recipe
|
||||
pm.PhotoPath = t.PhotoPath
|
||||
|
||||
possibleFormats := []string{
|
||||
"2006-01-02",
|
||||
time.Layout,
|
||||
time.ANSIC,
|
||||
time.UnixDate,
|
||||
time.RubyDate,
|
||||
time.RFC822,
|
||||
time.RFC822Z,
|
||||
time.RFC850,
|
||||
time.RFC1123,
|
||||
time.RFC1123Z,
|
||||
time.RFC3339,
|
||||
time.RFC3339Nano,
|
||||
time.Stamp,
|
||||
time.StampMilli,
|
||||
time.StampMicro,
|
||||
time.StampNano,
|
||||
}
|
||||
|
||||
for _, format := range possibleFormats {
|
||||
if t, err := time.Parse(format, s.Date); err == nil {
|
||||
pm.Date = t
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type PostWebmetions struct {
|
||||
Incoming []WebmentionIn `ymal:"incoming"`
|
||||
Outgoing []WebmentionOut `ymal:"outgoing"`
|
||||
}
|
||||
|
||||
func (post *GenericPost) Id() string {
|
||||
return post.id
|
||||
}
|
||||
|
||||
func (post *GenericPost) User() *User {
|
||||
return post.user
|
||||
}
|
||||
|
||||
func (post *GenericPost) Dir() string {
|
||||
return path.Join(post.user.Dir(), "public", post.id)
|
||||
}
|
||||
|
||||
func (post *GenericPost) IncomingWebmentionsFile() string {
|
||||
return path.Join(post.Dir(), "incoming_webmentions.yml")
|
||||
}
|
||||
|
||||
func (post *GenericPost) OutgoingWebmentionsFile() string {
|
||||
return path.Join(post.Dir(), "outgoing_webmentions.yml")
|
||||
}
|
||||
|
||||
func (post *GenericPost) MediaDir() string {
|
||||
return path.Join(post.Dir(), "media")
|
||||
}
|
||||
|
||||
func (post *GenericPost) UrlPath() string {
|
||||
return post.user.UrlPath() + "posts/" + post.id + "/"
|
||||
}
|
||||
|
||||
func (post *GenericPost) FullUrl() string {
|
||||
return post.user.FullUrl() + "posts/" + post.id + "/"
|
||||
}
|
||||
|
||||
func (post *GenericPost) UrlMediaPath(filename string) string {
|
||||
return post.UrlPath() + "media/" + filename
|
||||
}
|
||||
|
||||
func (post *GenericPost) Title() string {
|
||||
return post.Meta().Title
|
||||
}
|
||||
|
||||
func (post *GenericPost) ContentFile() string {
|
||||
return path.Join(post.Dir(), "index.md")
|
||||
}
|
||||
|
||||
func (post *GenericPost) Meta() PostMeta {
|
||||
if !post.metaLoaded {
|
||||
post.LoadMeta()
|
||||
}
|
||||
return post.meta
|
||||
}
|
||||
|
||||
func (post *GenericPost) Content() []byte {
|
||||
// read file
|
||||
data, _ := os.ReadFile(post.ContentFile())
|
||||
return data
|
||||
}
|
||||
|
||||
func (post *GenericPost) RenderedContent() string {
|
||||
data := post.Content()
|
||||
|
||||
// trim yaml block
|
||||
// TODO this can be done nicer
|
||||
trimmedData := bytes.TrimSpace(data)
|
||||
// ensure that data ends with a newline
|
||||
trimmedData = append(trimmedData, []byte("\n")...)
|
||||
// check first line is ---
|
||||
if string(trimmedData[0:4]) == "---\n" {
|
||||
trimmedData = trimmedData[4:]
|
||||
// find --- end
|
||||
end := bytes.Index(trimmedData, []byte("\n---\n"))
|
||||
if end != -1 {
|
||||
data = trimmedData[end+5:]
|
||||
}
|
||||
}
|
||||
|
||||
options := goldmark.WithRendererOptions()
|
||||
if config, _ := post.user.repo.Config(); config.AllowRawHtml {
|
||||
options = goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
)
|
||||
}
|
||||
|
||||
markdown := goldmark.New(
|
||||
options,
|
||||
goldmark.WithExtensions(
|
||||
// meta.Meta,
|
||||
extension.GFM,
|
||||
),
|
||||
)
|
||||
var buf bytes.Buffer
|
||||
context := parser.NewContext()
|
||||
if err := markdown.Convert(data, &buf, parser.WithContext(context)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
|
||||
}
|
||||
|
||||
func (post *GenericPost) Aliases() []string {
|
||||
return post.Meta().Aliases
|
||||
}
|
||||
|
||||
func (post *GenericPost) LoadMeta() error {
|
||||
data := post.Content()
|
||||
|
||||
// get yaml metadata block
|
||||
meta := PostMeta{}
|
||||
trimmedData := bytes.TrimSpace(data)
|
||||
// ensure that data ends with a newline
|
||||
trimmedData = append(trimmedData, []byte("\n")...)
|
||||
// check first line is ---
|
||||
if string(trimmedData[0:4]) == "---\n" {
|
||||
trimmedData = trimmedData[4:]
|
||||
// find --- end
|
||||
end := bytes.Index(trimmedData, []byte("---\n"))
|
||||
if end != -1 {
|
||||
metaData := trimmedData[:end]
|
||||
err := yaml.Unmarshal(metaData, &meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post.meta = meta
|
||||
return nil
|
||||
}
|
||||
|
||||
func (post *GenericPost) IncomingWebmentions() []WebmentionIn {
|
||||
// return parsed webmentions
|
||||
fileName := post.IncomingWebmentionsFile()
|
||||
if !fileExists(fileName) {
|
||||
return []WebmentionIn{}
|
||||
}
|
||||
|
||||
webmentions := []WebmentionIn{}
|
||||
loadFromYaml(fileName, &webmentions)
|
||||
|
||||
return webmentions
|
||||
}
|
||||
|
||||
func (post *GenericPost) OutgoingWebmentions() []WebmentionOut {
|
||||
// return parsed webmentions
|
||||
fileName := post.OutgoingWebmentionsFile()
|
||||
if !fileExists(fileName) {
|
||||
return []WebmentionOut{}
|
||||
}
|
||||
|
||||
webmentions := []WebmentionOut{}
|
||||
loadFromYaml(fileName, &webmentions)
|
||||
|
||||
return webmentions
|
||||
}
|
||||
|
||||
// PersistWebmentionOutgoing persists incoming webmention
|
||||
func (post *GenericPost) PersistIncomingWebmention(webmention WebmentionIn) error {
|
||||
post.wmLock.Lock()
|
||||
defer post.wmLock.Unlock()
|
||||
|
||||
wms := post.IncomingWebmentions()
|
||||
|
||||
// if target is not in status, add it
|
||||
replaced := false
|
||||
for i, t := range wms {
|
||||
if t.Source == webmention.Source {
|
||||
wms[i].UpdateWith(webmention)
|
||||
replaced = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !replaced {
|
||||
wms = append(wms, webmention)
|
||||
}
|
||||
|
||||
err := saveToYaml(post.IncomingWebmentionsFile(), wms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PersistOutgoingWebmention persists a webmention to the webmention file.
|
||||
func (post *GenericPost) PersistOutgoingWebmention(webmention *WebmentionOut) error {
|
||||
post.wmLock.Lock()
|
||||
defer post.wmLock.Unlock()
|
||||
|
||||
wms := post.OutgoingWebmentions()
|
||||
|
||||
// if target is not in webmention, add it
|
||||
replaced := false
|
||||
for i, t := range wms {
|
||||
if t.Target == webmention.Target {
|
||||
wms[i].UpdateWith(*webmention)
|
||||
replaced = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !replaced {
|
||||
wms = append(wms, *webmention)
|
||||
}
|
||||
|
||||
err := saveToYaml(post.OutgoingWebmentionsFile(), wms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (post *GenericPost) AddIncomingWebmention(source string) error {
|
||||
// Check if file already exists
|
||||
wm := WebmentionIn{
|
||||
Source: source,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
go post.EnrichWebmention(wm)
|
||||
}()
|
||||
return post.PersistIncomingWebmention(wm)
|
||||
}
|
||||
|
||||
func (post *GenericPost) EnrichWebmention(webmention WebmentionIn) error {
|
||||
resp, err := post.user.repo.HttpClient.Get(webmention.Source)
|
||||
if err == nil {
|
||||
entry, err := post.user.repo.Parser.ParseHEntry(resp)
|
||||
if err == nil {
|
||||
webmention.Title = entry.Title
|
||||
return post.PersistIncomingWebmention(webmention)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (post *GenericPost) ApprovedIncomingWebmentions() []WebmentionIn {
|
||||
webmentions := post.IncomingWebmentions()
|
||||
approved := []WebmentionIn{}
|
||||
for _, webmention := range webmentions {
|
||||
if webmention.ApprovalStatus == "approved" {
|
||||
approved = append(approved, webmention)
|
||||
}
|
||||
}
|
||||
|
||||
// sort by retrieved date
|
||||
sort.Slice(approved, func(i, j int) bool {
|
||||
return approved[i].RetrievedAt.After(approved[j].RetrievedAt)
|
||||
})
|
||||
return approved
|
||||
}
|
||||
|
||||
// ScanForLinks scans the post content for links and adds them to the
|
||||
// `status.yml` file for the post. The links are not scanned by this function.
|
||||
func (post *GenericPost) ScanForLinks() error {
|
||||
// this could be done in markdown parsing, but I don't want to
|
||||
// rely on goldmark for this (yet)
|
||||
postHtml := post.RenderedContent()
|
||||
links, _ := post.user.repo.Parser.ParseLinksFromString(postHtml)
|
||||
// add reply url if set
|
||||
if post.Meta().Reply.Url != "" {
|
||||
links = append(links, post.Meta().Reply.Url)
|
||||
}
|
||||
for _, link := range links {
|
||||
post.PersistOutgoingWebmention(&WebmentionOut{
|
||||
Target: link,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (post *GenericPost) SendWebmention(webmention WebmentionOut) error {
|
||||
defer post.PersistOutgoingWebmention(&webmention)
|
||||
|
||||
// if last scan is less than 7 days ago, don't send webmention
|
||||
if webmention.ScannedAt.After(time.Now().Add(-7*24*time.Hour)) && !webmention.Supported {
|
||||
return errors.New("did not scan. Last scan was less than 7 days ago")
|
||||
}
|
||||
|
||||
webmention.ScannedAt = time.Now()
|
||||
|
||||
resp, err := post.user.repo.HttpClient.Get(webmention.Target)
|
||||
if err != nil {
|
||||
webmention.Supported = false
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint, err := post.user.repo.Parser.GetWebmentionEndpoint(resp)
|
||||
if err != nil {
|
||||
webmention.Supported = false
|
||||
return err
|
||||
}
|
||||
webmention.Supported = true
|
||||
|
||||
// send webmention
|
||||
payload := url.Values{}
|
||||
payload.Set("source", post.FullUrl())
|
||||
payload.Set("target", webmention.Target)
|
||||
_, err = post.user.repo.HttpClient.PostForm(endpoint, payload)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update webmention status
|
||||
webmention.LastSentAt = time.Now()
|
||||
return nil
|
||||
}
|
531
post_test.go
531
post_test.go
|
@ -1,531 +0,0 @@
|
|||
package owl_test
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"h4kor/owl-blogs/test/mocks"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCanGetPostTitle(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
result := post.Title()
|
||||
assertions.AssertEqual(t, result, "testpost")
|
||||
}
|
||||
|
||||
func TestMediaDir(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
result := post.MediaDir()
|
||||
assertions.AssertEqual(t, result, path.Join(post.Dir(), "media"))
|
||||
}
|
||||
|
||||
func TestPostUrlPath(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
expected := "/user/" + user.Name() + "/posts/" + post.Id() + "/"
|
||||
assertions.AssertEqual(t, post.UrlPath(), expected)
|
||||
}
|
||||
|
||||
func TestPostFullUrl(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
expected := "http://localhost:8080/user/" + user.Name() + "/posts/" + post.Id() + "/"
|
||||
assertions.AssertEqual(t, post.FullUrl(), expected)
|
||||
}
|
||||
|
||||
func TestPostUrlMediaPath(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
expected := "/user/" + user.Name() + "/posts/" + post.Id() + "/media/data.png"
|
||||
assertions.AssertEqual(t, post.UrlMediaPath("data.png"), expected)
|
||||
}
|
||||
|
||||
func TestPostUrlMediaPathWithSubDir(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
expected := "/user/" + user.Name() + "/posts/" + post.Id() + "/media/foo/data.png"
|
||||
assertions.AssertEqual(t, post.UrlMediaPath("foo/data.png"), expected)
|
||||
}
|
||||
|
||||
func TestDraftInMetaData(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
content := "---\n"
|
||||
content += "title: test\n"
|
||||
content += "draft: true\n"
|
||||
content += "---\n"
|
||||
content += "\n"
|
||||
content += "Write your post here.\n"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
meta := post.Meta()
|
||||
assertions.AssertEqual(t, meta.Draft, true)
|
||||
}
|
||||
|
||||
func TestNoRawHTMLIfDisallowedByRepo(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
content := "---\n"
|
||||
content += "title: test\n"
|
||||
content += "draft: true\n"
|
||||
content += "---\n"
|
||||
content += "\n"
|
||||
content += "<script>alert('foo')</script>\n"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
html := post.RenderedContent()
|
||||
assertions.AssertNotContains(t, html, "<script>")
|
||||
}
|
||||
|
||||
func TestRawHTMLIfAllowedByRepo(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{AllowRawHtml: true})
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
content := "---\n"
|
||||
content += "title: test\n"
|
||||
content += "draft: true\n"
|
||||
content += "---\n"
|
||||
content += "\n"
|
||||
content += "<script>alert('foo')</script>\n"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
html := post.RenderedContent()
|
||||
assertions.AssertContains(t, html, "<script>")
|
||||
}
|
||||
|
||||
func TestMeta(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{AllowRawHtml: true})
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: test\n"
|
||||
content += "draft: true\n"
|
||||
content += "date: Wed, 17 Aug 2022 10:50:02 +0000\n"
|
||||
content += "aliases:\n"
|
||||
content += " - foo/bar/\n"
|
||||
content += "---\n"
|
||||
content += "\n"
|
||||
content += "<script>alert('foo')</script>\n"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
assertions.AssertEqual(t, post.Meta().Title, "test")
|
||||
assertions.AssertLen(t, post.Meta().Aliases, 1)
|
||||
assertions.AssertEqual(t, post.Meta().Draft, true)
|
||||
assertions.AssertEqual(t, post.Meta().Date.Format(time.RFC1123Z), "Wed, 17 Aug 2022 10:50:02 +0000")
|
||||
assertions.AssertEqual(t, post.Meta().Draft, true)
|
||||
}
|
||||
|
||||
///
|
||||
/// Webmention
|
||||
///
|
||||
|
||||
func TestPersistIncomingWebmention(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
webmention := owl.WebmentionIn{
|
||||
Source: "http://example.com/source",
|
||||
}
|
||||
err := post.PersistIncomingWebmention(webmention)
|
||||
assertions.AssertNoError(t, err, "Error persisting webmention")
|
||||
mentions := post.IncomingWebmentions()
|
||||
assertions.AssertLen(t, mentions, 1)
|
||||
assertions.AssertEqual(t, mentions[0].Source, webmention.Source)
|
||||
}
|
||||
|
||||
func TestAddIncomingWebmentionCreatesFile(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
repo.HttpClient = &mocks.MockHttpClient{}
|
||||
repo.Parser = &mocks.MockHtmlParser{}
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
err := post.AddIncomingWebmention("https://example.com")
|
||||
assertions.AssertNoError(t, err, "Error adding webmention")
|
||||
|
||||
mentions := post.IncomingWebmentions()
|
||||
assertions.AssertLen(t, mentions, 1)
|
||||
}
|
||||
|
||||
func TestAddIncomingWebmentionNotOverwritingWebmention(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
repo.HttpClient = &mocks.MockHttpClient{}
|
||||
repo.Parser = &mocks.MockHtmlParser{}
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
post.PersistIncomingWebmention(owl.WebmentionIn{
|
||||
Source: "https://example.com",
|
||||
ApprovalStatus: "approved",
|
||||
})
|
||||
|
||||
post.AddIncomingWebmention("https://example.com")
|
||||
|
||||
mentions := post.IncomingWebmentions()
|
||||
assertions.AssertLen(t, mentions, 1)
|
||||
|
||||
assertions.AssertEqual(t, mentions[0].ApprovalStatus, "approved")
|
||||
}
|
||||
|
||||
func TestEnrichAddsTitle(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
repo.HttpClient = &mocks.MockHttpClient{}
|
||||
repo.Parser = &mocks.MockHtmlParser{}
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
post.AddIncomingWebmention("https://example.com")
|
||||
post.EnrichWebmention(owl.WebmentionIn{Source: "https://example.com"})
|
||||
|
||||
mentions := post.IncomingWebmentions()
|
||||
assertions.AssertLen(t, mentions, 1)
|
||||
assertions.AssertEqual(t, mentions[0].Title, "Mock Title")
|
||||
}
|
||||
|
||||
func TestApprovedIncomingWebmentions(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
webmention := owl.WebmentionIn{
|
||||
Source: "http://example.com/source",
|
||||
ApprovalStatus: "approved",
|
||||
RetrievedAt: time.Now(),
|
||||
}
|
||||
post.PersistIncomingWebmention(webmention)
|
||||
webmention = owl.WebmentionIn{
|
||||
Source: "http://example.com/source2",
|
||||
ApprovalStatus: "",
|
||||
RetrievedAt: time.Now().Add(time.Hour * -1),
|
||||
}
|
||||
post.PersistIncomingWebmention(webmention)
|
||||
webmention = owl.WebmentionIn{
|
||||
Source: "http://example.com/source3",
|
||||
ApprovalStatus: "approved",
|
||||
RetrievedAt: time.Now().Add(time.Hour * -2),
|
||||
}
|
||||
post.PersistIncomingWebmention(webmention)
|
||||
webmention = owl.WebmentionIn{
|
||||
Source: "http://example.com/source4",
|
||||
ApprovalStatus: "rejected",
|
||||
RetrievedAt: time.Now().Add(time.Hour * -3),
|
||||
}
|
||||
post.PersistIncomingWebmention(webmention)
|
||||
|
||||
webmentions := post.ApprovedIncomingWebmentions()
|
||||
assertions.AssertLen(t, webmentions, 2)
|
||||
|
||||
assertions.AssertEqual(t, webmentions[0].Source, "http://example.com/source")
|
||||
assertions.AssertEqual(t, webmentions[1].Source, "http://example.com/source3")
|
||||
|
||||
}
|
||||
|
||||
func TestScanningForLinks(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: test\n"
|
||||
content += "date: Wed, 17 Aug 2022 10:50:02 +0000\n"
|
||||
content += "---\n"
|
||||
content += "\n"
|
||||
content += "[Hello](https://example.com/hello)\n"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
post.ScanForLinks()
|
||||
webmentions := post.OutgoingWebmentions()
|
||||
assertions.AssertLen(t, webmentions, 1)
|
||||
assertions.AssertEqual(t, webmentions[0].Target, "https://example.com/hello")
|
||||
}
|
||||
|
||||
func TestScanningForLinksDoesNotAddDuplicates(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: test\n"
|
||||
content += "date: Wed, 17 Aug 2022 10:50:02 +0000\n"
|
||||
content += "---\n"
|
||||
content += "\n"
|
||||
content += "[Hello](https://example.com/hello)\n"
|
||||
content += "[Hello](https://example.com/hello)\n"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
post.ScanForLinks()
|
||||
post.ScanForLinks()
|
||||
post.ScanForLinks()
|
||||
webmentions := post.OutgoingWebmentions()
|
||||
assertions.AssertLen(t, webmentions, 1)
|
||||
assertions.AssertEqual(t, webmentions[0].Target, "https://example.com/hello")
|
||||
}
|
||||
|
||||
func TestScanningForLinksDoesAddReplyUrl(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: test\n"
|
||||
content += "date: Wed, 17 Aug 2022 10:50:02 +0000\n"
|
||||
content += "reply:\n"
|
||||
content += " url: https://example.com/reply\n"
|
||||
content += "---\n"
|
||||
content += "\n"
|
||||
content += "Hi\n"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
post.ScanForLinks()
|
||||
webmentions := post.OutgoingWebmentions()
|
||||
assertions.AssertLen(t, webmentions, 1)
|
||||
assertions.AssertEqual(t, webmentions[0].Target, "https://example.com/reply")
|
||||
}
|
||||
|
||||
func TestCanSendWebmention(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
repo.HttpClient = &mocks.MockHttpClient{}
|
||||
repo.Parser = &mocks.MockHtmlParser{}
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
webmention := owl.WebmentionOut{
|
||||
Target: "http://example.com",
|
||||
}
|
||||
|
||||
err := post.SendWebmention(webmention)
|
||||
assertions.AssertNoError(t, err, "Error sending webmention")
|
||||
|
||||
webmentions := post.OutgoingWebmentions()
|
||||
|
||||
assertions.AssertLen(t, webmentions, 1)
|
||||
assertions.AssertEqual(t, webmentions[0].Target, "http://example.com")
|
||||
assertions.AssertEqual(t, webmentions[0].LastSentAt.IsZero(), false)
|
||||
}
|
||||
|
||||
func TestSendWebmentionOnlyScansOncePerWeek(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
repo.HttpClient = &mocks.MockHttpClient{}
|
||||
repo.Parser = &mocks.MockHtmlParser{}
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
webmention := owl.WebmentionOut{
|
||||
Target: "http://example.com",
|
||||
ScannedAt: time.Now().Add(time.Hour * -24 * 6),
|
||||
}
|
||||
|
||||
post.PersistOutgoingWebmention(&webmention)
|
||||
webmentions := post.OutgoingWebmentions()
|
||||
webmention = webmentions[0]
|
||||
|
||||
err := post.SendWebmention(webmention)
|
||||
assertions.AssertError(t, err, "Expected error, got nil")
|
||||
|
||||
webmentions = post.OutgoingWebmentions()
|
||||
|
||||
assertions.AssertLen(t, webmentions, 1)
|
||||
assertions.AssertEqual(t, webmentions[0].ScannedAt, webmention.ScannedAt)
|
||||
}
|
||||
|
||||
func TestSendingMultipleWebmentions(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
repo.HttpClient = &mocks.MockHttpClient{}
|
||||
repo.Parser = &mocks.MockHtmlParser{}
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(20)
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
go func(k int) {
|
||||
webmention := owl.WebmentionOut{
|
||||
Target: "http://example.com" + strconv.Itoa(k),
|
||||
}
|
||||
post.SendWebmention(webmention)
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
webmentions := post.OutgoingWebmentions()
|
||||
|
||||
assertions.AssertLen(t, webmentions, 20)
|
||||
}
|
||||
|
||||
func TestReceivingMultipleWebmentions(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
repo.HttpClient = &mocks.MockHttpClient{}
|
||||
repo.Parser = &mocks.MockHtmlParser{}
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(20)
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
go func(k int) {
|
||||
post.AddIncomingWebmention("http://example.com" + strconv.Itoa(k))
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
webmentions := post.IncomingWebmentions()
|
||||
|
||||
assertions.AssertLen(t, webmentions, 20)
|
||||
|
||||
}
|
||||
|
||||
func TestSendingAndReceivingMultipleWebmentions(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
repo.HttpClient = &mocks.MockHttpClient{}
|
||||
repo.Parser = &mocks.MockHtmlParser{}
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(40)
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
go func(k int) {
|
||||
post.AddIncomingWebmention("http://example.com" + strconv.Itoa(k))
|
||||
wg.Done()
|
||||
}(i)
|
||||
go func(k int) {
|
||||
webmention := owl.WebmentionOut{
|
||||
Target: "http://example.com" + strconv.Itoa(k),
|
||||
}
|
||||
post.SendWebmention(webmention)
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
ins := post.IncomingWebmentions()
|
||||
outs := post.OutgoingWebmentions()
|
||||
|
||||
assertions.AssertLen(t, ins, 20)
|
||||
assertions.AssertLen(t, outs, 20)
|
||||
}
|
||||
|
||||
func TestComplexParallelWebmentions(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
repo.HttpClient = &mocks.MockHttpClient{}
|
||||
repo.Parser = &mocks.MockParseLinksHtmlParser{
|
||||
Links: []string{
|
||||
"http://example.com/1",
|
||||
"http://example.com/2",
|
||||
"http://example.com/3",
|
||||
},
|
||||
}
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(60)
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
go func(k int) {
|
||||
post.AddIncomingWebmention("http://example.com/" + strconv.Itoa(k))
|
||||
wg.Done()
|
||||
}(i)
|
||||
go func(k int) {
|
||||
webmention := owl.WebmentionOut{
|
||||
Target: "http://example.com/" + strconv.Itoa(k),
|
||||
}
|
||||
post.SendWebmention(webmention)
|
||||
wg.Done()
|
||||
}(i)
|
||||
go func() {
|
||||
post.ScanForLinks()
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
ins := post.IncomingWebmentions()
|
||||
outs := post.OutgoingWebmentions()
|
||||
|
||||
assertions.AssertLen(t, ins, 20)
|
||||
assertions.AssertLen(t, outs, 20)
|
||||
}
|
||||
|
||||
func TestPostWithoutContent(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{}, "")
|
||||
|
||||
result := post.RenderedContent()
|
||||
assertions.AssertEqual(t, result, "")
|
||||
}
|
||||
|
||||
// func TestComplexParallelSimulatedProcessesWebmentions(t *testing.T) {
|
||||
// repoName := testRepoName()
|
||||
// repo, _ := owl.CreateRepository(repoName, owl.RepoConfig{})
|
||||
// repo.HttpClient = &mocks.MockHttpClient{}
|
||||
// repo.Parser = &MockParseLinksHtmlParser{
|
||||
// Links: []string{
|
||||
// "http://example.com/1",
|
||||
// "http://example.com/2",
|
||||
// "http://example.com/3",
|
||||
// },
|
||||
// }
|
||||
// user, _ := repo.CreateUser("testuser")
|
||||
// post, _ := user.CreateNewPostFull(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
// wg := sync.WaitGroup{}
|
||||
// wg.Add(40)
|
||||
|
||||
// for i := 0; i < 20; i++ {
|
||||
// go func(k int) {
|
||||
// defer wg.Done()
|
||||
// fRepo, _ := owl.OpenRepository(repoName)
|
||||
// fUser, _ := fRepo.GetUser("testuser")
|
||||
// fPost, err := fUser.GetPost(post.Id())
|
||||
// if err != nil {
|
||||
// t.Error(err)
|
||||
// return
|
||||
// }
|
||||
// fPost.AddIncomingWebmention("http://example.com/" + strconv.Itoa(k))
|
||||
// }(i)
|
||||
// go func(k int) {
|
||||
// defer wg.Done()
|
||||
// fRepo, _ := owl.OpenRepository(repoName)
|
||||
// fUser, _ := fRepo.GetUser("testuser")
|
||||
// fPost, err := fUser.GetPost(post.Id())
|
||||
// if err != nil {
|
||||
// t.Error(err)
|
||||
// return
|
||||
// }
|
||||
// webmention := owl.WebmentionOut{
|
||||
// Target: "http://example.com/" + strconv.Itoa(k),
|
||||
// }
|
||||
// fPost.SendWebmention(webmention)
|
||||
// }(i)
|
||||
// }
|
||||
|
||||
// wg.Wait()
|
||||
|
||||
// ins := post.IncomingWebmentions()
|
||||
|
||||
// if len(ins) != 20 {
|
||||
// t.Errorf("Expected 20 webmentions, got %d", len(ins))
|
||||
// }
|
||||
|
||||
// outs := post.OutgoingWebmentions()
|
||||
|
||||
// if len(outs) != 20 {
|
||||
// t.Errorf("Expected 20 webmentions, got %d", len(outs))
|
||||
// }
|
||||
// }
|
|
@ -1,2 +0,0 @@
|
|||
docker build . -t git.libove.org/h4kor/owl-blogs:$1
|
||||
docker push git.libove.org/h4kor/owl-blogs:$1
|
254
renderer.go
254
renderer.go
|
@ -1,254 +0,0 @@
|
|||
package owl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PageContent struct {
|
||||
Title string
|
||||
Description string
|
||||
Content template.HTML
|
||||
Type string
|
||||
SelfUrl string
|
||||
}
|
||||
|
||||
type PostRenderData struct {
|
||||
Title string
|
||||
Post Post
|
||||
Content template.HTML
|
||||
}
|
||||
|
||||
type AuthRequestData struct {
|
||||
Me string
|
||||
ClientId string
|
||||
RedirectUri string
|
||||
State string
|
||||
Scope string
|
||||
Scopes []string // Split version of scope. filled by rendering function.
|
||||
ResponseType string
|
||||
CodeChallenge string
|
||||
CodeChallengeMethod string
|
||||
User User
|
||||
CsrfToken string
|
||||
}
|
||||
|
||||
type EditorViewData struct {
|
||||
User User
|
||||
Error string
|
||||
CsrfToken string
|
||||
}
|
||||
|
||||
type ErrorMessage struct {
|
||||
Error string
|
||||
Message string
|
||||
}
|
||||
|
||||
func noescape(str string) template.HTML {
|
||||
return template.HTML(str)
|
||||
}
|
||||
|
||||
func listUrl(user User, id string) string {
|
||||
return user.ListUrl(PostList{
|
||||
Id: id,
|
||||
})
|
||||
}
|
||||
|
||||
func postUrl(user User, id string) string {
|
||||
post, _ := user.GetPost(id)
|
||||
return post.UrlPath()
|
||||
}
|
||||
|
||||
func renderEmbedTemplate(templateFile string, data interface{}) (string, error) {
|
||||
templateStr, err := embed_files.ReadFile(templateFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return renderTemplateStr(templateStr, data)
|
||||
}
|
||||
|
||||
func renderTemplateStr(templateStr []byte, data interface{}) (string, error) {
|
||||
t, err := template.New("_").Funcs(template.FuncMap{
|
||||
"noescape": noescape,
|
||||
"listUrl": listUrl,
|
||||
"postUrl": postUrl,
|
||||
}).Parse(string(templateStr))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var html bytes.Buffer
|
||||
err = t.Execute(&html, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return html.String(), nil
|
||||
}
|
||||
|
||||
func renderIntoBaseTemplate(user User, data PageContent) (string, error) {
|
||||
baseTemplate, _ := user.Template()
|
||||
t, err := template.New("index").Funcs(template.FuncMap{
|
||||
"noescape": noescape,
|
||||
"listUrl": listUrl,
|
||||
"postUrl": postUrl,
|
||||
}).Parse(baseTemplate)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
full_data := struct {
|
||||
Title string
|
||||
Description string
|
||||
Content template.HTML
|
||||
Type string
|
||||
SelfUrl string
|
||||
User User
|
||||
}{
|
||||
Title: data.Title,
|
||||
Description: data.Description,
|
||||
Content: data.Content,
|
||||
Type: data.Type,
|
||||
SelfUrl: data.SelfUrl,
|
||||
User: user,
|
||||
}
|
||||
|
||||
var html bytes.Buffer
|
||||
err = t.Execute(&html, full_data)
|
||||
return html.String(), err
|
||||
}
|
||||
|
||||
func renderPostContent(post Post) (string, error) {
|
||||
buf := post.RenderedContent()
|
||||
postHtml, err := renderEmbedTemplate(
|
||||
fmt.Sprintf("embed/%s/detail.html", post.TemplateDir()),
|
||||
PostRenderData{
|
||||
Title: post.Title(),
|
||||
Post: post,
|
||||
Content: template.HTML(buf),
|
||||
},
|
||||
)
|
||||
return postHtml, err
|
||||
}
|
||||
|
||||
func RenderPost(post Post) (string, error) {
|
||||
postHtml, err := renderPostContent(post)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return renderIntoBaseTemplate(*post.User(), PageContent{
|
||||
Title: post.Title(),
|
||||
Description: post.Meta().Description,
|
||||
Content: template.HTML(postHtml),
|
||||
Type: "article",
|
||||
SelfUrl: post.FullUrl(),
|
||||
})
|
||||
}
|
||||
|
||||
func RenderIndexPage(user User) (string, error) {
|
||||
posts, _ := user.PrimaryFeedPosts()
|
||||
|
||||
postHtml, err := renderEmbedTemplate("embed/post-list.html", posts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return renderIntoBaseTemplate(user, PageContent{
|
||||
Title: "Index",
|
||||
Content: template.HTML(postHtml),
|
||||
})
|
||||
}
|
||||
|
||||
func RenderPostList(user User, list *PostList) (string, error) {
|
||||
posts, _ := user.GetPostsOfList(*list)
|
||||
var postHtml string
|
||||
var err error
|
||||
if list.ListType == "photo" {
|
||||
postHtml, err = renderEmbedTemplate("embed/post-list-photo.html", posts)
|
||||
} else {
|
||||
postHtml, err = renderEmbedTemplate("embed/post-list.html", posts)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return renderIntoBaseTemplate(user, PageContent{
|
||||
Title: "Index",
|
||||
Content: template.HTML(postHtml),
|
||||
})
|
||||
}
|
||||
|
||||
func RenderUserAuthPage(reqData AuthRequestData) (string, error) {
|
||||
reqData.Scopes = strings.Split(reqData.Scope, " ")
|
||||
authHtml, err := renderEmbedTemplate("embed/auth.html", reqData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return renderIntoBaseTemplate(reqData.User, PageContent{
|
||||
Title: "Auth",
|
||||
Content: template.HTML(authHtml),
|
||||
})
|
||||
}
|
||||
|
||||
func RenderUserError(user User, error ErrorMessage) (string, error) {
|
||||
errHtml, err := renderEmbedTemplate("embed/error.html", error)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return renderIntoBaseTemplate(user, PageContent{
|
||||
Title: "Error",
|
||||
Content: template.HTML(errHtml),
|
||||
})
|
||||
}
|
||||
|
||||
func RenderUserList(repo Repository) (string, error) {
|
||||
baseTemplate, _ := repo.Template()
|
||||
users, _ := repo.Users()
|
||||
userHtml, err := renderEmbedTemplate("embed/user-list.html", users)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data := PageContent{
|
||||
Title: "Index",
|
||||
Content: template.HTML(userHtml),
|
||||
}
|
||||
|
||||
return renderTemplateStr([]byte(baseTemplate), data)
|
||||
}
|
||||
|
||||
func RenderLoginPage(user User, error_type string, csrfToken string) (string, error) {
|
||||
loginHtml, err := renderEmbedTemplate("embed/editor/login.html", EditorViewData{
|
||||
User: user,
|
||||
Error: error_type,
|
||||
CsrfToken: csrfToken,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return renderIntoBaseTemplate(user, PageContent{
|
||||
Title: "Login",
|
||||
Content: template.HTML(loginHtml),
|
||||
})
|
||||
}
|
||||
|
||||
func RenderEditorPage(user User, csrfToken string) (string, error) {
|
||||
editorHtml, err := renderEmbedTemplate("embed/editor/editor.html", EditorViewData{
|
||||
User: user,
|
||||
CsrfToken: csrfToken,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return renderIntoBaseTemplate(user, PageContent{
|
||||
Title: "Editor",
|
||||
Content: template.HTML(editorHtml),
|
||||
})
|
||||
}
|
505
renderer_test.go
505
renderer_test.go
|
@ -1,505 +0,0 @@
|
|||
package owl_test
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCanRenderPost(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
result, err := owl.RenderPost(post)
|
||||
|
||||
assertions.AssertNoError(t, err, "Error rendering post")
|
||||
assertions.AssertContains(t, result, "testpost")
|
||||
|
||||
}
|
||||
|
||||
func TestRenderOneMe(t *testing.T) {
|
||||
user := getTestUser()
|
||||
config := user.Config()
|
||||
config.Me = append(config.Me, owl.UserMe{
|
||||
Name: "Twitter",
|
||||
Url: "https://twitter.com/testhandle",
|
||||
})
|
||||
|
||||
user.SetConfig(config)
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
result, err := owl.RenderPost(post)
|
||||
|
||||
assertions.AssertNoError(t, err, "Error rendering post")
|
||||
assertions.AssertContains(t, result, "href=\"https://twitter.com/testhandle\" rel=\"me\"")
|
||||
|
||||
}
|
||||
|
||||
func TestRenderTwoMe(t *testing.T) {
|
||||
user := getTestUser()
|
||||
config := user.Config()
|
||||
config.Me = append(config.Me, owl.UserMe{
|
||||
Name: "Twitter",
|
||||
Url: "https://twitter.com/testhandle",
|
||||
})
|
||||
config.Me = append(config.Me, owl.UserMe{
|
||||
Name: "Github",
|
||||
Url: "https://github.com/testhandle",
|
||||
})
|
||||
|
||||
user.SetConfig(config)
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
result, err := owl.RenderPost(post)
|
||||
|
||||
assertions.AssertNoError(t, err, "Error rendering post")
|
||||
assertions.AssertContains(t, result, "href=\"https://twitter.com/testhandle\" rel=\"me\"")
|
||||
assertions.AssertContains(t, result, "href=\"https://github.com/testhandle\" rel=\"me\"")
|
||||
|
||||
}
|
||||
|
||||
func TestRenderPostHEntry(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
result, _ := owl.RenderPost(post)
|
||||
assertions.AssertContains(t, result, "class=\"h-entry\"")
|
||||
assertions.AssertContains(t, result, "class=\"p-name\"")
|
||||
assertions.AssertContains(t, result, "class=\"e-content\"")
|
||||
|
||||
}
|
||||
|
||||
func TestRendererUsesBaseTemplate(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
result, err := owl.RenderPost(post)
|
||||
|
||||
assertions.AssertNoError(t, err, "Error rendering post")
|
||||
assertions.AssertContains(t, result, "<html")
|
||||
}
|
||||
|
||||
func TestCanRenderPostList(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost1"}, "")
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost2"}, "")
|
||||
result, err := owl.RenderPostList(user, &owl.PostList{
|
||||
Id: "testlist",
|
||||
Title: "Test List",
|
||||
Include: []string{
|
||||
"article",
|
||||
},
|
||||
})
|
||||
assertions.AssertNoError(t, err, "Error rendering post list")
|
||||
assertions.AssertContains(t, result, "testpost1")
|
||||
assertions.AssertContains(t, result, "testpost2")
|
||||
}
|
||||
|
||||
func TestCanRenderPostListNotIncludeOther(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.CreateNewPost(owl.PostMeta{Title: "testpost1", Type: "article"}, "testpost1")
|
||||
user.CreateNewPost(owl.PostMeta{Title: "testpost2", Type: "note"}, "testpost2")
|
||||
result, _ := owl.RenderPostList(user, &owl.PostList{
|
||||
Id: "testlist",
|
||||
Title: "Test List",
|
||||
Include: []string{
|
||||
"article",
|
||||
},
|
||||
})
|
||||
assertions.AssertContains(t, result, "testpost1")
|
||||
assertions.AssertNotContains(t, result, "testpost2")
|
||||
}
|
||||
|
||||
func TestCanRenderPostListNotIncludeMultiple(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.CreateNewPost(owl.PostMeta{Title: "testpost1", Type: "article"}, "testpost1")
|
||||
user.CreateNewPost(owl.PostMeta{Title: "testpost2", Type: "note"}, "testpost2")
|
||||
user.CreateNewPost(owl.PostMeta{Title: "testpost3", Type: "recipe"}, "testpost3")
|
||||
result, _ := owl.RenderPostList(user, &owl.PostList{
|
||||
Id: "testlist",
|
||||
Title: "Test List",
|
||||
Include: []string{
|
||||
"article", "recipe",
|
||||
},
|
||||
})
|
||||
assertions.AssertContains(t, result, "testpost1")
|
||||
assertions.AssertNotContains(t, result, "testpost2")
|
||||
assertions.AssertContains(t, result, "testpost3")
|
||||
}
|
||||
|
||||
func TestCanRenderIndexPage(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost1"}, "")
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost2"}, "")
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
assertions.AssertContains(t, result, "testpost1")
|
||||
assertions.AssertContains(t, result, "testpost2")
|
||||
}
|
||||
|
||||
func TestCanRenderIndexPageNoTitle(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{}, "hi")
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
assertions.AssertContains(t, result, post.Id())
|
||||
}
|
||||
|
||||
func TestRenderNoteAsFullContent(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.CreateNewPost(owl.PostMeta{Type: "note"}, "This is a note")
|
||||
result, _ := owl.RenderPostList(user, &owl.PostList{
|
||||
Include: []string{"note"},
|
||||
})
|
||||
assertions.AssertContains(t, result, "This is a note")
|
||||
assertions.AssertNotContains(t, result, "<p>This is a note")
|
||||
}
|
||||
|
||||
func TestIndexPageContainsHFeedContainer(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost1"}, "")
|
||||
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
assertions.AssertContains(t, result, "<div class=\"h-feed\">")
|
||||
}
|
||||
|
||||
func TestIndexPageContainsHEntryAndUUrl(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost1"}, "")
|
||||
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
assertions.AssertContains(t, result, "class=\"h-entry\"")
|
||||
assertions.AssertContains(t, result, "class=\"u-url\"")
|
||||
|
||||
}
|
||||
|
||||
func TestIndexPageDoesNotContainsArticle(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article"}, "hi")
|
||||
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
assertions.AssertContains(t, result, "class=\"h-entry\"")
|
||||
assertions.AssertContains(t, result, "class=\"u-url\"")
|
||||
}
|
||||
|
||||
func TestIndexPageDoesNotContainsReply(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.CreateNewPost(owl.PostMeta{Type: "reply", Reply: owl.ReplyData{Url: "https://example.com/post"}}, "hi")
|
||||
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
assertions.AssertContains(t, result, "class=\"h-entry\"")
|
||||
assertions.AssertContains(t, result, "class=\"u-url\"")
|
||||
}
|
||||
|
||||
func TestRenderIndexPageWithBrokenBaseTemplate(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost1"}, "")
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost2"}, "")
|
||||
|
||||
os.WriteFile(path.Join(user.Dir(), "meta/base.html"), []byte("{{content}}"), 0644)
|
||||
|
||||
_, err := owl.RenderIndexPage(user)
|
||||
assertions.AssertError(t, err, "Expected error rendering index page")
|
||||
}
|
||||
|
||||
func TestRenderUserList(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
repo.CreateUser("user1")
|
||||
repo.CreateUser("user2")
|
||||
|
||||
result, err := owl.RenderUserList(repo)
|
||||
assertions.AssertNoError(t, err, "Error rendering user list")
|
||||
assertions.AssertContains(t, result, "user1")
|
||||
assertions.AssertContains(t, result, "user2")
|
||||
}
|
||||
|
||||
func TestRendersHeaderTitle(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.SetConfig(owl.UserConfig{
|
||||
Title: "Test Title",
|
||||
SubTitle: "Test SubTitle",
|
||||
HeaderColor: "#ff1337",
|
||||
})
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
result, _ := owl.RenderPost(post)
|
||||
assertions.AssertContains(t, result, "Test Title")
|
||||
assertions.AssertContains(t, result, "Test SubTitle")
|
||||
assertions.AssertContains(t, result, "#ff1337")
|
||||
}
|
||||
|
||||
func TestRenderPostIncludesRelToWebMention(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
result, _ := owl.RenderPost(post)
|
||||
assertions.AssertContains(t, result, "rel=\"webmention\"")
|
||||
|
||||
assertions.AssertContains(t, result, "href=\""+user.WebmentionUrl()+"\"")
|
||||
}
|
||||
|
||||
func TestRenderPostAddsLinksToApprovedWebmention(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
webmention := owl.WebmentionIn{
|
||||
Source: "http://example.com/source3",
|
||||
Title: "Test Title",
|
||||
ApprovalStatus: "approved",
|
||||
RetrievedAt: time.Now().Add(time.Hour * -2),
|
||||
}
|
||||
post.PersistIncomingWebmention(webmention)
|
||||
webmention = owl.WebmentionIn{
|
||||
Source: "http://example.com/source4",
|
||||
ApprovalStatus: "rejected",
|
||||
RetrievedAt: time.Now().Add(time.Hour * -3),
|
||||
}
|
||||
post.PersistIncomingWebmention(webmention)
|
||||
|
||||
result, _ := owl.RenderPost(post)
|
||||
assertions.AssertContains(t, result, "http://example.com/source3")
|
||||
assertions.AssertContains(t, result, "Test Title")
|
||||
assertions.AssertNotContains(t, result, "http://example.com/source4")
|
||||
|
||||
}
|
||||
|
||||
func TestRenderPostNotMentioningWebmentionsIfNoAvail(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
result, _ := owl.RenderPost(post)
|
||||
|
||||
assertions.AssertNotContains(t, result, "Webmention")
|
||||
|
||||
}
|
||||
|
||||
func TestRenderIncludesFullUrl(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
result, _ := owl.RenderPost(post)
|
||||
|
||||
assertions.AssertContains(t, result, "class=\"u-url\"")
|
||||
assertions.AssertContains(t, result, post.FullUrl())
|
||||
}
|
||||
|
||||
func TestAddAvatarIfExist(t *testing.T) {
|
||||
user := getTestUser()
|
||||
os.WriteFile(path.Join(user.MediaDir(), "avatar.png"), []byte("test"), 0644)
|
||||
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
assertions.AssertContains(t, result, "avatar.png")
|
||||
}
|
||||
|
||||
func TestAuthorNameInPost(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.SetConfig(owl.UserConfig{
|
||||
Title: "Test Title",
|
||||
SubTitle: "Test SubTitle",
|
||||
HeaderColor: "#ff1337",
|
||||
AuthorName: "Test Author",
|
||||
})
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
result, _ := owl.RenderPost(post)
|
||||
assertions.AssertContains(t, result, "Test Author")
|
||||
}
|
||||
|
||||
func TestRenderReplyWithoutText(t *testing.T) {
|
||||
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{
|
||||
Type: "reply",
|
||||
Reply: owl.ReplyData{
|
||||
Url: "https://example.com/post",
|
||||
},
|
||||
}, "Hi ")
|
||||
|
||||
result, _ := owl.RenderPost(post)
|
||||
assertions.AssertContains(t, result, "https://example.com/post")
|
||||
}
|
||||
|
||||
func TestRenderReplyWithText(t *testing.T) {
|
||||
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{
|
||||
Type: "reply",
|
||||
Reply: owl.ReplyData{
|
||||
Url: "https://example.com/post",
|
||||
Text: "This is a reply",
|
||||
},
|
||||
}, "Hi ")
|
||||
|
||||
result, _ := owl.RenderPost(post)
|
||||
assertions.AssertContains(t, result, "https://example.com/post")
|
||||
|
||||
assertions.AssertContains(t, result, "This is a reply")
|
||||
}
|
||||
|
||||
func TestRengerPostContainsBookmark(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "bookmark", Bookmark: owl.BookmarkData{Url: "https://example.com/post"}}, "hi")
|
||||
|
||||
result, _ := owl.RenderPost(post)
|
||||
assertions.AssertContains(t, result, "https://example.com/post")
|
||||
}
|
||||
|
||||
func TestOpenGraphTags(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: The Rock\n"
|
||||
content += "description: Dwayne Johnson\n"
|
||||
content += "date: Wed, 17 Aug 2022 10:50:02 +0000\n"
|
||||
content += "---\n"
|
||||
content += "\n"
|
||||
content += "Hi \n"
|
||||
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
post, _ = user.GetPost(post.Id())
|
||||
result, _ := owl.RenderPost(post)
|
||||
|
||||
assertions.AssertContains(t, result, "<meta property=\"og:title\" content=\"The Rock\" />")
|
||||
assertions.AssertContains(t, result, "<meta property=\"og:description\" content=\"Dwayne Johnson\" />")
|
||||
assertions.AssertContains(t, result, "<meta property=\"og:type\" content=\"article\" />")
|
||||
assertions.AssertContains(t, result, "<meta property=\"og:url\" content=\""+post.FullUrl()+"\" />")
|
||||
|
||||
}
|
||||
|
||||
func TestAddFaviconIfExist(t *testing.T) {
|
||||
user := getTestUser()
|
||||
os.WriteFile(path.Join(user.MediaDir(), "favicon.png"), []byte("test"), 0644)
|
||||
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
assertions.AssertContains(t, result, "favicon.png")
|
||||
}
|
||||
|
||||
func TestRenderUserAuth(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.ResetPassword("test")
|
||||
result, err := owl.RenderUserAuthPage(owl.AuthRequestData{
|
||||
User: user,
|
||||
})
|
||||
assertions.AssertNoError(t, err, "Error rendering user auth page")
|
||||
assertions.AssertContains(t, result, "<form")
|
||||
}
|
||||
|
||||
func TestRenderUserAuthIncludesClientId(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.ResetPassword("test")
|
||||
result, err := owl.RenderUserAuthPage(owl.AuthRequestData{
|
||||
User: user,
|
||||
ClientId: "https://example.com/",
|
||||
})
|
||||
assertions.AssertNoError(t, err, "Error rendering user auth page")
|
||||
assertions.AssertContains(t, result, "https://example.com/")
|
||||
}
|
||||
|
||||
func TestRenderUserAuthHiddenFields(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.ResetPassword("test")
|
||||
result, err := owl.RenderUserAuthPage(owl.AuthRequestData{
|
||||
User: user,
|
||||
ClientId: "https://example.com/",
|
||||
RedirectUri: "https://example.com/redirect",
|
||||
ResponseType: "code",
|
||||
State: "teststate",
|
||||
})
|
||||
assertions.AssertNoError(t, err, "Error rendering user auth page")
|
||||
assertions.AssertContains(t, result, "name=\"client_id\" value=\"https://example.com/\"")
|
||||
assertions.AssertContains(t, result, "name=\"redirect_uri\" value=\"https://example.com/redirect\"")
|
||||
assertions.AssertContains(t, result, "name=\"response_type\" value=\"code\"")
|
||||
assertions.AssertContains(t, result, "name=\"state\" value=\"teststate\"")
|
||||
}
|
||||
|
||||
func TestRenderHeaderMenuListItem(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.AddHeaderMenuItem(owl.MenuItem{
|
||||
Title: "Test Entry",
|
||||
List: "test",
|
||||
})
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
assertions.AssertContains(t, result, "Test Entry")
|
||||
assertions.AssertContains(t, result, "/lists/test")
|
||||
}
|
||||
|
||||
func TestRenderHeaderMenuUrlItem(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.AddHeaderMenuItem(owl.MenuItem{
|
||||
Title: "Test Entry",
|
||||
Url: "https://example.com",
|
||||
})
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
assertions.AssertContains(t, result, "Test Entry")
|
||||
assertions.AssertContains(t, result, "https://example.com")
|
||||
}
|
||||
|
||||
func TestRenderHeaderMenuPost(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
user.AddHeaderMenuItem(owl.MenuItem{
|
||||
Title: "Test Entry",
|
||||
Post: post.Id(),
|
||||
})
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
assertions.AssertContains(t, result, "Test Entry")
|
||||
assertions.AssertContains(t, result, post.UrlPath())
|
||||
}
|
||||
|
||||
func TestRenderFooterMenuListItem(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.AddFooterMenuItem(owl.MenuItem{
|
||||
Title: "Test Entry",
|
||||
List: "test",
|
||||
})
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
assertions.AssertContains(t, result, "Test Entry")
|
||||
assertions.AssertContains(t, result, "/lists/test")
|
||||
}
|
||||
|
||||
func TestRenderFooterMenuUrlItem(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.AddFooterMenuItem(owl.MenuItem{
|
||||
Title: "Test Entry",
|
||||
Url: "https://example.com",
|
||||
})
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
assertions.AssertContains(t, result, "Test Entry")
|
||||
assertions.AssertContains(t, result, "https://example.com")
|
||||
}
|
||||
|
||||
func TestRenderFooterMenuPost(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{
|
||||
Type: "private",
|
||||
}, "")
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
assertions.AssertNotContains(t, result, "Test Entry")
|
||||
assertions.AssertNotContains(t, result, post.UrlPath())
|
||||
user.AddFooterMenuItem(owl.MenuItem{
|
||||
Title: "Test Entry",
|
||||
Post: post.Id(),
|
||||
})
|
||||
result, _ = owl.RenderIndexPage(user)
|
||||
assertions.AssertContains(t, result, "Test Entry")
|
||||
assertions.AssertContains(t, result, post.UrlPath())
|
||||
}
|
||||
|
||||
func TestRecipePost(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, err := user.CreateNewPost(owl.PostMeta{
|
||||
Type: "recipe",
|
||||
Title: "test recipe",
|
||||
Recipe: owl.RecipeData{
|
||||
Yield: "1 loaf",
|
||||
Ingredients: []string{
|
||||
"1 cup flour",
|
||||
"1 cup water",
|
||||
},
|
||||
Duration: "1 hour",
|
||||
},
|
||||
}, "")
|
||||
assertions.AssertNoError(t, err, "Error creating post")
|
||||
|
||||
result, err := owl.RenderPost(post)
|
||||
assertions.AssertNoError(t, err, "Error rendering post")
|
||||
assertions.AssertContains(t, result, "1 loaf")
|
||||
assertions.AssertContains(t, result, "1 cup flour")
|
||||
assertions.AssertContains(t, result, "1 cup water")
|
||||
assertions.AssertContains(t, result, "1 hour")
|
||||
}
|
184
repository.go
184
repository.go
|
@ -1,184 +0,0 @@
|
|||
package owl
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
//go:embed embed/initial/base.html
|
||||
var base_template string
|
||||
|
||||
var VERSION = "0.0.1"
|
||||
|
||||
type Repository struct {
|
||||
name string
|
||||
HttpClient HttpClient
|
||||
Parser HtmlParser
|
||||
}
|
||||
|
||||
type RepoConfig struct {
|
||||
Domain string `yaml:"domain"`
|
||||
SingleUser string `yaml:"single_user"`
|
||||
AllowRawHtml bool `yaml:"allow_raw_html"`
|
||||
}
|
||||
|
||||
func CreateRepository(name string, config RepoConfig) (Repository, error) {
|
||||
newRepo := Repository{name: name, Parser: OwlHtmlParser{}, HttpClient: &OwlHttpClient{}}
|
||||
// check if repository already exists
|
||||
if dirExists(newRepo.Dir()) {
|
||||
return Repository{}, fmt.Errorf("Repository already exists")
|
||||
}
|
||||
|
||||
os.Mkdir(newRepo.Dir(), 0755)
|
||||
os.Mkdir(newRepo.UsersDir(), 0755)
|
||||
os.Mkdir(newRepo.StaticDir(), 0755)
|
||||
|
||||
// create config file
|
||||
if config.Domain == "" {
|
||||
config.Domain = "http://localhost:8080"
|
||||
}
|
||||
saveToYaml(path.Join(newRepo.Dir(), "config.yml"), config)
|
||||
|
||||
// copy all files from static_files embed.FS to StaticDir
|
||||
staticFiles, _ := embed_files.ReadDir("embed/initial/static")
|
||||
for _, file := range staticFiles {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
src_data, _ := embed_files.ReadFile("embed/initial/static/" + file.Name())
|
||||
os.WriteFile(newRepo.StaticDir()+"/"+file.Name(), src_data, 0644)
|
||||
}
|
||||
|
||||
// copy repo/ to newRepo.Dir()
|
||||
init_files, _ := embed_files.ReadDir("embed/initial/repo")
|
||||
for _, file := range init_files {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
src_data, _ := embed_files.ReadFile("embed/initial/repo/" + file.Name())
|
||||
os.WriteFile(newRepo.Dir()+"/"+file.Name(), src_data, 0644)
|
||||
}
|
||||
return newRepo, nil
|
||||
}
|
||||
|
||||
func OpenRepository(name string) (Repository, error) {
|
||||
|
||||
repo := Repository{name: name, Parser: OwlHtmlParser{}, HttpClient: &OwlHttpClient{}}
|
||||
if !dirExists(repo.Dir()) {
|
||||
return Repository{}, fmt.Errorf("Repository does not exist: " + repo.Dir())
|
||||
}
|
||||
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
func (repo Repository) Dir() string {
|
||||
return repo.name
|
||||
}
|
||||
|
||||
func (repo Repository) StaticDir() string {
|
||||
return path.Join(repo.Dir(), "static")
|
||||
}
|
||||
|
||||
func (repo Repository) UsersDir() string {
|
||||
return path.Join(repo.Dir(), "users")
|
||||
}
|
||||
|
||||
func (repo Repository) UserUrlPath(user User) string {
|
||||
config, _ := repo.Config()
|
||||
if config.SingleUser != "" {
|
||||
return "/"
|
||||
}
|
||||
return "/user/" + user.name + "/"
|
||||
}
|
||||
|
||||
func (repo Repository) FullUrl() string {
|
||||
config, _ := repo.Config()
|
||||
return config.Domain
|
||||
}
|
||||
|
||||
func (repo Repository) Template() (string, error) {
|
||||
// load base.html
|
||||
path := path.Join(repo.Dir(), "base.html")
|
||||
base_html, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(base_html), nil
|
||||
}
|
||||
|
||||
func (repo Repository) Users() ([]User, error) {
|
||||
config, _ := repo.Config()
|
||||
if config.SingleUser != "" {
|
||||
return []User{{repo: &repo, name: config.SingleUser}}, nil
|
||||
}
|
||||
|
||||
userNames := listDir(repo.UsersDir())
|
||||
users := make([]User, len(userNames))
|
||||
for i, name := range userNames {
|
||||
users[i] = User{repo: &repo, name: name}
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (repo *Repository) CreateUser(name string) (User, error) {
|
||||
new_user := User{repo: repo, name: name}
|
||||
// check if user already exists
|
||||
if dirExists(new_user.Dir()) {
|
||||
return User{}, fmt.Errorf("User already exists")
|
||||
}
|
||||
|
||||
// creates repo/name folder if it doesn't exist
|
||||
user_dir := new_user.Dir()
|
||||
os.Mkdir(user_dir, 0755)
|
||||
// create folders
|
||||
os.Mkdir(path.Join(user_dir, "meta"), 0755)
|
||||
os.Mkdir(path.Join(user_dir, "public"), 0755)
|
||||
os.Mkdir(path.Join(user_dir, "media"), 0755)
|
||||
|
||||
// create Meta files
|
||||
os.WriteFile(path.Join(user_dir, "meta", "VERSION"), []byte(VERSION), 0644)
|
||||
os.WriteFile(path.Join(user_dir, "meta", "base.html"), []byte(base_template), 0644)
|
||||
|
||||
saveToYaml(new_user.ConfigFile(), UserConfig{
|
||||
Title: name,
|
||||
SubTitle: "",
|
||||
HeaderColor: "#bdd6be",
|
||||
})
|
||||
|
||||
return new_user, nil
|
||||
}
|
||||
|
||||
func (repo Repository) GetUser(name string) (User, error) {
|
||||
user := User{repo: &repo, name: name}
|
||||
if !dirExists(user.Dir()) {
|
||||
return User{}, fmt.Errorf("User does not exist")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (repo Repository) PostAliases() (map[string]Post, error) {
|
||||
users, err := repo.Users()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aliases := make(map[string]Post)
|
||||
for _, user := range users {
|
||||
user_aliases, err := user.PostAliases()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for alias, post := range user_aliases {
|
||||
aliases[alias] = post
|
||||
}
|
||||
}
|
||||
return aliases, nil
|
||||
}
|
||||
|
||||
func (repo Repository) Config() (RepoConfig, error) {
|
||||
meta := RepoConfig{}
|
||||
err := loadFromYaml(path.Join(repo.Dir(), "config.yml"), &meta)
|
||||
return meta, err
|
||||
|
||||
}
|
|
@ -1,257 +0,0 @@
|
|||
package owl_test
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCanCreateRepository(t *testing.T) {
|
||||
repoName := testRepoName()
|
||||
_, err := owl.CreateRepository(repoName, owl.RepoConfig{})
|
||||
assertions.AssertNoError(t, err, "Error creating repository: ")
|
||||
|
||||
}
|
||||
|
||||
func TestCannotCreateExistingRepository(t *testing.T) {
|
||||
repoName := testRepoName()
|
||||
owl.CreateRepository(repoName, owl.RepoConfig{})
|
||||
_, err := owl.CreateRepository(repoName, owl.RepoConfig{})
|
||||
assertions.AssertError(t, err, "No error returned when creating existing repository")
|
||||
}
|
||||
|
||||
func TestCanCreateANewUser(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
_, err := os.Stat(path.Join(user.Dir(), ""))
|
||||
assertions.AssertNoError(t, err, "Error creating user: ")
|
||||
}
|
||||
|
||||
func TestCannotRecreateExisitingUser(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
userName := randomUserName()
|
||||
repo.CreateUser(userName)
|
||||
_, err := repo.CreateUser(userName)
|
||||
assertions.AssertError(t, err, "No error returned when creating existing user")
|
||||
}
|
||||
|
||||
func TestCreateUserAddsVersionFile(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
_, err := os.Stat(path.Join(user.Dir(), "/meta/VERSION"))
|
||||
assertions.AssertNoError(t, err, "Version file not created")
|
||||
}
|
||||
|
||||
func TestCreateUserAddsBaseHtmlFile(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
_, err := os.Stat(path.Join(user.Dir(), "/meta/base.html"))
|
||||
assertions.AssertNoError(t, err, "Base html file not created")
|
||||
}
|
||||
|
||||
func TestCreateUserAddConfigYml(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
_, err := os.Stat(path.Join(user.Dir(), "/meta/config.yml"))
|
||||
assertions.AssertNoError(t, err, "Config file not created")
|
||||
}
|
||||
|
||||
func TestCreateUserAddsPublicFolder(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
_, err := os.Stat(path.Join(user.Dir(), "/public"))
|
||||
assertions.AssertNoError(t, err, "Public folder not created")
|
||||
}
|
||||
|
||||
func TestCanListRepoUsers(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user1, _ := repo.CreateUser(randomUserName())
|
||||
user2, _ := repo.CreateUser(randomUserName())
|
||||
// Create a new post
|
||||
users, _ := repo.Users()
|
||||
assertions.AssertLen(t, users, 2)
|
||||
for _, user := range users {
|
||||
assertions.AssertNot(
|
||||
t,
|
||||
user.Name() != user1.Name() && user.Name() != user2.Name(),
|
||||
"User found: "+user.Name(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanOpenRepository(t *testing.T) {
|
||||
// Create a new user
|
||||
repoName := testRepoName()
|
||||
repo, _ := owl.CreateRepository(repoName, owl.RepoConfig{})
|
||||
// Open the repository
|
||||
repo2, err := owl.OpenRepository(repoName)
|
||||
assertions.AssertNoError(t, err, "Error opening repository: ")
|
||||
assertions.Assert(t, repo2.Dir() == repo.Dir(), "Repository directories do not match")
|
||||
}
|
||||
|
||||
func TestCannotOpenNonExisitingRepo(t *testing.T) {
|
||||
_, err := owl.OpenRepository(testRepoName())
|
||||
assertions.AssertError(t, err, "No error returned when opening non-existing repository")
|
||||
}
|
||||
|
||||
func TestGetUser(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
// Get the user
|
||||
user2, err := repo.GetUser(user.Name())
|
||||
assertions.AssertNoError(t, err, "Error getting user: ")
|
||||
assertions.Assert(t, user2.Name() == user.Name(), "User names do not match")
|
||||
}
|
||||
|
||||
func TestCannotGetNonexistingUser(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
_, err := repo.GetUser(randomUserName())
|
||||
assertions.AssertError(t, err, "No error returned when getting non-existing user")
|
||||
}
|
||||
|
||||
func TestGetStaticDirOfRepo(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
// Get the user
|
||||
staticDir := repo.StaticDir()
|
||||
assertions.Assert(t, staticDir != "", "Static dir is empty")
|
||||
}
|
||||
|
||||
func TestNewRepoGetsStaticFiles(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
_, err := os.Stat(repo.StaticDir())
|
||||
assertions.AssertNoError(t, err, "Static dir not created")
|
||||
dir, _ := os.Open(repo.StaticDir())
|
||||
defer dir.Close()
|
||||
files, _ := dir.Readdirnames(-1)
|
||||
|
||||
assertions.AssertLen(t, files, 1)
|
||||
}
|
||||
|
||||
func TestNewRepoGetsStaticFilesPicoCSSWithContent(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
file, err := os.Open(path.Join(repo.StaticDir(), "pico.min.css"))
|
||||
assertions.AssertNoError(t, err, "Error opening pico.min.css")
|
||||
// check that the file has content
|
||||
stat, _ := file.Stat()
|
||||
assertions.Assert(t, stat.Size() > 0, "pico.min.css is empty")
|
||||
}
|
||||
|
||||
func TestNewRepoGetsBaseHtml(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
_, err := os.Stat(path.Join(repo.Dir(), "/base.html"))
|
||||
assertions.AssertNoError(t, err, "Base html file not found")
|
||||
}
|
||||
|
||||
func TestCanGetRepoTemplate(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
// Get the user
|
||||
template, err := repo.Template()
|
||||
assertions.AssertNoError(t, err, "Error getting template: ")
|
||||
assertions.Assert(t, template != "", "Template is empty")
|
||||
}
|
||||
|
||||
func TestCanOpenRepositoryInSingleUserMode(t *testing.T) {
|
||||
// Create a new user
|
||||
repoName := testRepoName()
|
||||
userName := randomUserName()
|
||||
created_repo, _ := owl.CreateRepository(repoName, owl.RepoConfig{SingleUser: userName})
|
||||
created_repo.CreateUser(userName)
|
||||
created_repo.CreateUser(randomUserName())
|
||||
created_repo.CreateUser(randomUserName())
|
||||
|
||||
// Open the repository
|
||||
repo, _ := owl.OpenRepository(repoName)
|
||||
|
||||
users, _ := repo.Users()
|
||||
assertions.AssertLen(t, users, 1)
|
||||
assertions.Assert(t, users[0].Name() == userName, "User name does not match")
|
||||
}
|
||||
|
||||
func TestSingleUserRepoUserUrlPathIsSimple(t *testing.T) {
|
||||
// Create a new user
|
||||
repoName := testRepoName()
|
||||
userName := randomUserName()
|
||||
created_repo, _ := owl.CreateRepository(repoName, owl.RepoConfig{SingleUser: userName})
|
||||
created_repo.CreateUser(userName)
|
||||
|
||||
// Open the repository
|
||||
repo, _ := owl.OpenRepository(repoName)
|
||||
user, _ := repo.GetUser(userName)
|
||||
assertions.Assert(t, user.UrlPath() == "/", "User url path is not /")
|
||||
}
|
||||
|
||||
func TestCanGetMapWithAllPostAliases(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "test-1"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - /foo/bar\n"
|
||||
content += " - /foo/baz\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
posts, _ := user.PublishedPosts()
|
||||
assertions.AssertLen(t, posts, 1)
|
||||
|
||||
var aliases map[string]owl.Post
|
||||
aliases, err := repo.PostAliases()
|
||||
assertions.AssertNoError(t, err, "Error getting post aliases: ")
|
||||
assertions.AssertMapLen(t, aliases, 2)
|
||||
assertions.Assert(t, aliases["/foo/bar"] != nil, "Alias '/foo/bar' not found")
|
||||
assertions.Assert(t, aliases["/foo/baz"] != nil, "Alias '/foo/baz' not found")
|
||||
|
||||
}
|
||||
|
||||
func TestAliasesHaveCorrectPost(t *testing.T) {
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
post1, _ := user.CreateNewPost(owl.PostMeta{Title: "test-1"}, "")
|
||||
post2, _ := user.CreateNewPost(owl.PostMeta{Title: "test-2"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - /foo/1\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post1.ContentFile(), []byte(content), 0644)
|
||||
|
||||
content = "---\n"
|
||||
content += "title: Test\n"
|
||||
content += "aliases: \n"
|
||||
content += " - /foo/2\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post2.ContentFile(), []byte(content), 0644)
|
||||
|
||||
posts, _ := user.PublishedPosts()
|
||||
assertions.AssertLen(t, posts, 2)
|
||||
|
||||
var aliases map[string]owl.Post
|
||||
aliases, err := repo.PostAliases()
|
||||
assertions.AssertNoError(t, err, "Error getting post aliases: ")
|
||||
assertions.AssertMapLen(t, aliases, 2)
|
||||
assertions.Assert(t, aliases["/foo/1"].Id() == post1.Id(), "Alias '/foo/1' does not point to post 1")
|
||||
assertions.Assert(t, aliases["/foo/2"].Id() == post2.Id(), "Alias '/foo/2' does not point to post 2")
|
||||
|
||||
}
|
65
rss.go
65
rss.go
|
@ -1,65 +0,0 @@
|
|||
package owl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RSS struct {
|
||||
XMLName xml.Name `xml:"rss"`
|
||||
Version string `xml:"version,attr"`
|
||||
Channel RSSChannel `xml:"channel"`
|
||||
}
|
||||
|
||||
type RSSChannel struct {
|
||||
Title string `xml:"title"`
|
||||
Link string `xml:"link"`
|
||||
Description string `xml:"description"`
|
||||
Items []RSSItem `xml:"item"`
|
||||
}
|
||||
|
||||
type RSSItem struct {
|
||||
Guid string `xml:"guid"`
|
||||
Title string `xml:"title"`
|
||||
Link string `xml:"link"`
|
||||
PubDate string `xml:"pubDate"`
|
||||
Description string `xml:"description"`
|
||||
}
|
||||
|
||||
func RenderRSSFeed(user User) (string, error) {
|
||||
|
||||
config := user.Config()
|
||||
|
||||
rss := RSS{
|
||||
Version: "2.0",
|
||||
Channel: RSSChannel{
|
||||
Title: config.Title,
|
||||
Link: user.FullUrl(),
|
||||
Description: config.SubTitle,
|
||||
Items: make([]RSSItem, 0),
|
||||
},
|
||||
}
|
||||
|
||||
posts, _ := user.PrimaryFeedPosts()
|
||||
for _, post := range posts {
|
||||
meta := post.Meta()
|
||||
content, _ := renderPostContent(post)
|
||||
rss.Channel.Items = append(rss.Channel.Items, RSSItem{
|
||||
Guid: post.FullUrl(),
|
||||
Title: post.Title(),
|
||||
Link: post.FullUrl(),
|
||||
PubDate: meta.Date.Format(time.RFC1123Z),
|
||||
Description: content,
|
||||
})
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err := xml.NewEncoder(buf).Encode(rss)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return xml.Header + buf.String(), nil
|
||||
|
||||
}
|
68
rss_test.go
68
rss_test.go
|
@ -1,68 +0,0 @@
|
|||
package owl_test
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRenderRSSFeedMeta(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.SetConfig(owl.UserConfig{
|
||||
Title: "Test Title",
|
||||
SubTitle: "Test SubTitle",
|
||||
})
|
||||
res, err := owl.RenderRSSFeed(user)
|
||||
assertions.AssertNoError(t, err, "Error rendering RSS feed")
|
||||
assertions.AssertContains(t, res, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
|
||||
assertions.AssertContains(t, res, "<rss version=\"2.0\">")
|
||||
|
||||
}
|
||||
|
||||
func TestRenderRSSFeedUserData(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.SetConfig(owl.UserConfig{
|
||||
Title: "Test Title",
|
||||
SubTitle: "Test SubTitle",
|
||||
})
|
||||
res, err := owl.RenderRSSFeed(user)
|
||||
assertions.AssertNoError(t, err, "Error rendering RSS feed")
|
||||
assertions.AssertContains(t, res, "Test Title")
|
||||
assertions.AssertContains(t, res, "Test SubTitle")
|
||||
assertions.AssertContains(t, res, "http://localhost:8080/user/")
|
||||
}
|
||||
|
||||
func TestRenderRSSFeedPostData(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "testpost"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test Post\n"
|
||||
content += "date: 2015-01-01\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
res, err := owl.RenderRSSFeed(user)
|
||||
assertions.AssertNoError(t, err, "Error rendering RSS feed")
|
||||
assertions.AssertContains(t, res, "Test Post")
|
||||
assertions.AssertContains(t, res, post.FullUrl())
|
||||
assertions.AssertContains(t, res, "Thu, 01 Jan 2015 00:00:00 +0000")
|
||||
}
|
||||
|
||||
func TestRenderRSSFeedPostDataWithoutDate(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Title: "testpost"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test Post\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
res, err := owl.RenderRSSFeed(user)
|
||||
assertions.AssertNoError(t, err, "Error rendering RSS feed")
|
||||
assertions.AssertContains(t, res, "Test Post")
|
||||
assertions.AssertContains(t, res, post.FullUrl())
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
package assertions
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Assert(t *testing.T, condition bool, message string) {
|
||||
t.Helper()
|
||||
if !condition {
|
||||
t.Errorf(message)
|
||||
}
|
||||
}
|
||||
|
||||
func AssertNot(t *testing.T, condition bool, message string) {
|
||||
t.Helper()
|
||||
if condition {
|
||||
t.Errorf(message)
|
||||
}
|
||||
}
|
||||
|
||||
func AssertContains(t *testing.T, containing string, search string) {
|
||||
t.Helper()
|
||||
if !strings.Contains(containing, search) {
|
||||
t.Errorf("Expected '%s' to contain '%s'", containing, search)
|
||||
}
|
||||
}
|
||||
|
||||
func AssertArrayContains[T comparable](t *testing.T, list []T, search T) {
|
||||
t.Helper()
|
||||
for _, item := range list {
|
||||
if item == search {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("Expected '%v' to be in '%v'", search, list)
|
||||
}
|
||||
|
||||
func AssertNotContains(t *testing.T, containing string, search string) {
|
||||
t.Helper()
|
||||
if strings.Contains(containing, search) {
|
||||
t.Errorf("Expected '%s' to not contain '%s'", containing, search)
|
||||
}
|
||||
}
|
||||
|
||||
func AssertNoError(t *testing.T, err error, message string) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Errorf(message+": %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func AssertError(t *testing.T, err error, message string) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Errorf(message)
|
||||
}
|
||||
}
|
||||
|
||||
func AssertLen[T any](t *testing.T, list []T, expected int) {
|
||||
t.Helper()
|
||||
if len(list) != expected {
|
||||
t.Errorf("Expected list to have length %d, got %d", expected, len(list))
|
||||
}
|
||||
}
|
||||
|
||||
func AssertMapLen[T any, S comparable](t *testing.T, list map[S]T, expected int) {
|
||||
t.Helper()
|
||||
if len(list) != expected {
|
||||
t.Errorf("Expected list to have length %d, got %d", expected, len(list))
|
||||
}
|
||||
}
|
||||
|
||||
func AssertEqual[T comparable](t *testing.T, actual T, expected T) {
|
||||
t.Helper()
|
||||
if actual != expected {
|
||||
t.Errorf("Expected '%v', got '%v'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func AssertNotEqual[T comparable](t *testing.T, actual T, expected T) {
|
||||
t.Helper()
|
||||
if actual == expected {
|
||||
t.Errorf("Expected '%v' to not be '%v'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func AssertStatus(t *testing.T, rr *httptest.ResponseRecorder, expStatus int) {
|
||||
if status := rr.Code; status != expStatus {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, expStatus)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func AssertLessThan(t *testing.T, actual int, expected int) {
|
||||
t.Helper()
|
||||
if actual >= expected {
|
||||
t.Errorf("Expected '%d' to be less than '%d'", actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func AssertGreaterThan(t *testing.T, actual int, expected int) {
|
||||
t.Helper()
|
||||
if actual <= expected {
|
||||
t.Errorf("Expected '%d' to be greater than '%d'", actual, expected)
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type MockHtmlParser struct{}
|
||||
|
||||
func (*MockHtmlParser) ParseHEntry(resp *http.Response) (owl.ParsedHEntry, error) {
|
||||
return owl.ParsedHEntry{Title: "Mock Title"}, nil
|
||||
|
||||
}
|
||||
func (*MockHtmlParser) ParseLinks(resp *http.Response) ([]string, error) {
|
||||
return []string{"http://example.com"}, nil
|
||||
|
||||
}
|
||||
func (*MockHtmlParser) ParseLinksFromString(string) ([]string, error) {
|
||||
return []string{"http://example.com"}, nil
|
||||
|
||||
}
|
||||
func (*MockHtmlParser) GetWebmentionEndpoint(resp *http.Response) (string, error) {
|
||||
return "http://example.com/webmention", nil
|
||||
|
||||
}
|
||||
func (*MockHtmlParser) GetRedirctUris(resp *http.Response) ([]string, error) {
|
||||
return []string{"http://example.com/redirect"}, nil
|
||||
}
|
||||
|
||||
type MockParseLinksHtmlParser struct {
|
||||
Links []string
|
||||
}
|
||||
|
||||
func (*MockParseLinksHtmlParser) ParseHEntry(resp *http.Response) (owl.ParsedHEntry, error) {
|
||||
return owl.ParsedHEntry{Title: "Mock Title"}, nil
|
||||
}
|
||||
func (parser *MockParseLinksHtmlParser) ParseLinks(resp *http.Response) ([]string, error) {
|
||||
return parser.Links, nil
|
||||
}
|
||||
func (parser *MockParseLinksHtmlParser) ParseLinksFromString(string) ([]string, error) {
|
||||
return parser.Links, nil
|
||||
}
|
||||
func (*MockParseLinksHtmlParser) GetWebmentionEndpoint(resp *http.Response) (string, error) {
|
||||
return "http://example.com/webmention", nil
|
||||
}
|
||||
func (parser *MockParseLinksHtmlParser) GetRedirctUris(resp *http.Response) ([]string, error) {
|
||||
return parser.Links, nil
|
||||
}
|
||||
|
||||
type MockHttpClient struct{}
|
||||
|
||||
func (*MockHttpClient) Get(url string) (resp *http.Response, err error) {
|
||||
return &http.Response{}, nil
|
||||
}
|
||||
func (*MockHttpClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
|
||||
|
||||
return &http.Response{}, nil
|
||||
}
|
||||
func (*MockHttpClient) PostForm(url string, data url.Values) (resp *http.Response, err error) {
|
||||
|
||||
return &http.Response{}, nil
|
||||
}
|
547
user.go
547
user.go
|
@ -1,547 +0,0 @@
|
|||
package owl
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
repo *Repository
|
||||
name string
|
||||
}
|
||||
|
||||
type UserConfig struct {
|
||||
Title string `yaml:"title"`
|
||||
SubTitle string `yaml:"subtitle"`
|
||||
HeaderColor string `yaml:"header_color"`
|
||||
AuthorName string `yaml:"author_name"`
|
||||
Me []UserMe `yaml:"me"`
|
||||
PassworHash string `yaml:"password_hash"`
|
||||
Lists []PostList `yaml:"lists"`
|
||||
PrimaryListInclude []string `yaml:"primary_list_include"`
|
||||
HeaderMenu []MenuItem `yaml:"header_menu"`
|
||||
FooterMenu []MenuItem `yaml:"footer_menu"`
|
||||
}
|
||||
|
||||
type PostList struct {
|
||||
Id string `yaml:"id"`
|
||||
Title string `yaml:"title"`
|
||||
Include []string `yaml:"include"`
|
||||
ListType string `yaml:"list_type"`
|
||||
}
|
||||
|
||||
type MenuItem struct {
|
||||
Title string `yaml:"title"`
|
||||
List string `yaml:"list"`
|
||||
Url string `yaml:"url"`
|
||||
Post string `yaml:"post"`
|
||||
}
|
||||
|
||||
func (l *PostList) ContainsType(t string) bool {
|
||||
for _, t2 := range l.Include {
|
||||
if t2 == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type UserMe struct {
|
||||
Name string `yaml:"name"`
|
||||
Url string `yaml:"url"`
|
||||
}
|
||||
|
||||
type AuthCode struct {
|
||||
Code string `yaml:"code"`
|
||||
ClientId string `yaml:"client_id"`
|
||||
RedirectUri string `yaml:"redirect_uri"`
|
||||
CodeChallenge string `yaml:"code_challenge"`
|
||||
CodeChallengeMethod string `yaml:"code_challenge_method"`
|
||||
Scope string `yaml:"scope"`
|
||||
Created time.Time `yaml:"created"`
|
||||
}
|
||||
|
||||
type AccessToken struct {
|
||||
Token string `yaml:"token"`
|
||||
Scope string `yaml:"scope"`
|
||||
ClientId string `yaml:"client_id"`
|
||||
RedirectUri string `yaml:"redirect_uri"`
|
||||
Created time.Time `yaml:"created"`
|
||||
ExpiresIn int `yaml:"expires_in"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
Id string `yaml:"id"`
|
||||
Created time.Time `yaml:"created"`
|
||||
ExpiresIn int `yaml:"expires_in"`
|
||||
}
|
||||
|
||||
func (user User) Dir() string {
|
||||
return path.Join(user.repo.UsersDir(), user.name)
|
||||
}
|
||||
|
||||
func (user User) UrlPath() string {
|
||||
return user.repo.UserUrlPath(user)
|
||||
}
|
||||
|
||||
func (user User) ListUrl(list PostList) string {
|
||||
url, _ := url.JoinPath(user.UrlPath(), "lists/"+list.Id+"/")
|
||||
return url
|
||||
}
|
||||
|
||||
func (user User) FullUrl() string {
|
||||
url, _ := url.JoinPath(user.repo.FullUrl(), user.UrlPath())
|
||||
return url
|
||||
}
|
||||
|
||||
func (user User) AuthUrl() string {
|
||||
if user.Config().PassworHash == "" {
|
||||
return ""
|
||||
}
|
||||
url, _ := url.JoinPath(user.FullUrl(), "auth/")
|
||||
return url
|
||||
}
|
||||
|
||||
func (user User) TokenUrl() string {
|
||||
url, _ := url.JoinPath(user.AuthUrl(), "token/")
|
||||
return url
|
||||
}
|
||||
|
||||
func (user User) IndieauthMetadataUrl() string {
|
||||
url, _ := url.JoinPath(user.FullUrl(), ".well-known/oauth-authorization-server")
|
||||
return url
|
||||
}
|
||||
|
||||
func (user User) WebmentionUrl() string {
|
||||
url, _ := url.JoinPath(user.FullUrl(), "webmention/")
|
||||
return url
|
||||
}
|
||||
|
||||
func (user User) MicropubUrl() string {
|
||||
url, _ := url.JoinPath(user.FullUrl(), "micropub/")
|
||||
return url
|
||||
}
|
||||
|
||||
func (user User) MediaUrl() string {
|
||||
url, _ := url.JoinPath(user.UrlPath(), "media")
|
||||
return url
|
||||
}
|
||||
|
||||
func (user User) EditorUrl() string {
|
||||
url, _ := url.JoinPath(user.UrlPath(), "editor/")
|
||||
return url
|
||||
}
|
||||
|
||||
func (user User) EditorLoginUrl() string {
|
||||
url, _ := url.JoinPath(user.UrlPath(), "editor/auth/")
|
||||
return url
|
||||
}
|
||||
|
||||
func (user User) PostDir() string {
|
||||
return path.Join(user.Dir(), "public")
|
||||
}
|
||||
|
||||
func (user User) MetaDir() string {
|
||||
return path.Join(user.Dir(), "meta")
|
||||
}
|
||||
|
||||
func (user User) MediaDir() string {
|
||||
return path.Join(user.Dir(), "media")
|
||||
}
|
||||
|
||||
func (user User) ConfigFile() string {
|
||||
return path.Join(user.MetaDir(), "config.yml")
|
||||
}
|
||||
|
||||
func (user User) AuthCodesFile() string {
|
||||
return path.Join(user.MetaDir(), "auth_codes.yml")
|
||||
}
|
||||
|
||||
func (user User) AccessTokensFile() string {
|
||||
return path.Join(user.MetaDir(), "access_tokens.yml")
|
||||
}
|
||||
|
||||
func (user User) SessionsFile() string {
|
||||
return path.Join(user.MetaDir(), "sessions.yml")
|
||||
}
|
||||
|
||||
func (user User) Name() string {
|
||||
return user.name
|
||||
}
|
||||
|
||||
func (user User) AvatarUrl() string {
|
||||
for _, ext := range []string{".jpg", ".jpeg", ".png", ".gif"} {
|
||||
if fileExists(path.Join(user.MediaDir(), "avatar"+ext)) {
|
||||
url, _ := url.JoinPath(user.MediaUrl(), "avatar"+ext)
|
||||
return url
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (user User) FaviconUrl() string {
|
||||
for _, ext := range []string{".jpg", ".jpeg", ".png", ".gif", ".ico"} {
|
||||
if fileExists(path.Join(user.MediaDir(), "favicon"+ext)) {
|
||||
url, _ := url.JoinPath(user.MediaUrl(), "favicon"+ext)
|
||||
return url
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (user User) AllPosts() ([]Post, error) {
|
||||
postFiles := listDir(path.Join(user.Dir(), "public"))
|
||||
posts := make([]Post, 0)
|
||||
for _, id := range postFiles {
|
||||
// if is a directory and has index.md, add to posts
|
||||
if dirExists(path.Join(user.Dir(), "public", id)) {
|
||||
if fileExists(path.Join(user.Dir(), "public", id, "index.md")) {
|
||||
post, _ := user.GetPost(id)
|
||||
posts = append(posts, post)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PostWithDate struct {
|
||||
post Post
|
||||
date time.Time
|
||||
}
|
||||
|
||||
postDates := make([]PostWithDate, len(posts))
|
||||
for i, post := range posts {
|
||||
meta := post.Meta()
|
||||
postDates[i] = PostWithDate{post: post, date: meta.Date}
|
||||
}
|
||||
|
||||
// sort posts by date
|
||||
sort.Slice(postDates, func(i, j int) bool {
|
||||
return postDates[i].date.After(postDates[j].date)
|
||||
})
|
||||
|
||||
for i, post := range postDates {
|
||||
posts[i] = post.post
|
||||
}
|
||||
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
func (user User) PublishedPosts() ([]Post, error) {
|
||||
posts, _ := user.AllPosts()
|
||||
|
||||
// remove drafts
|
||||
n := 0
|
||||
for _, post := range posts {
|
||||
meta := post.Meta()
|
||||
if !meta.Draft {
|
||||
posts[n] = post
|
||||
n++
|
||||
}
|
||||
}
|
||||
posts = posts[:n]
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
func (user User) PrimaryFeedPosts() ([]Post, error) {
|
||||
config := user.Config()
|
||||
include := config.PrimaryListInclude
|
||||
if len(include) == 0 {
|
||||
include = []string{"article", "reply"} // default before addition of this option
|
||||
}
|
||||
return user.GetPostsOfList(PostList{
|
||||
Id: "",
|
||||
Title: "",
|
||||
Include: include,
|
||||
})
|
||||
}
|
||||
|
||||
func (user User) GetPostsOfList(list PostList) ([]Post, error) {
|
||||
posts, _ := user.PublishedPosts()
|
||||
|
||||
// remove posts not included
|
||||
n := 0
|
||||
for _, post := range posts {
|
||||
meta := post.Meta()
|
||||
if list.ContainsType(meta.Type) {
|
||||
posts[n] = post
|
||||
n++
|
||||
}
|
||||
}
|
||||
posts = posts[:n]
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
func (user User) GetPost(id string) (Post, error) {
|
||||
// check if posts index.md exists
|
||||
if !fileExists(path.Join(user.Dir(), "public", id, "index.md")) {
|
||||
return &GenericPost{}, fmt.Errorf("post %s does not exist", id)
|
||||
}
|
||||
|
||||
post := GenericPost{user: &user, id: id}
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
func (user User) CreateNewPost(meta PostMeta, content string) (Post, error) {
|
||||
slugHint := meta.Title
|
||||
if slugHint == "" {
|
||||
slugHint = "note"
|
||||
}
|
||||
folder_name := toDirectoryName(slugHint)
|
||||
post_dir := path.Join(user.Dir(), "public", folder_name)
|
||||
|
||||
// if post already exists, add -n to the end of the name
|
||||
i := 0
|
||||
for {
|
||||
if dirExists(post_dir) {
|
||||
i++
|
||||
folder_name = toDirectoryName(fmt.Sprintf("%s-%d", slugHint, i))
|
||||
post_dir = path.Join(user.Dir(), "public", folder_name)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
post := GenericPost{user: &user, id: folder_name}
|
||||
|
||||
// if date is not set, set it to now
|
||||
if meta.Date.IsZero() {
|
||||
meta.Date = time.Now()
|
||||
}
|
||||
|
||||
initial_content := ""
|
||||
initial_content += "---\n"
|
||||
// write meta
|
||||
meta_bytes, err := yaml.Marshal(meta) // TODO: this should be down by the Post
|
||||
if err != nil {
|
||||
return &GenericPost{}, err
|
||||
}
|
||||
initial_content += string(meta_bytes)
|
||||
initial_content += "---\n"
|
||||
initial_content += "\n"
|
||||
initial_content += content
|
||||
|
||||
// create post file
|
||||
os.Mkdir(post_dir, 0755)
|
||||
os.WriteFile(post.ContentFile(), []byte(initial_content), 0644)
|
||||
// create media dir
|
||||
os.Mkdir(post.MediaDir(), 0755)
|
||||
return user.GetPost(post.Id())
|
||||
}
|
||||
|
||||
func (user User) Template() (string, error) {
|
||||
// load base.html
|
||||
path := path.Join(user.Dir(), "meta", "base.html")
|
||||
base_html, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(base_html), nil
|
||||
}
|
||||
|
||||
func (user User) Config() UserConfig {
|
||||
meta := UserConfig{}
|
||||
loadFromYaml(user.ConfigFile(), &meta)
|
||||
return meta
|
||||
}
|
||||
|
||||
func (user User) SetConfig(new_config UserConfig) error {
|
||||
return saveToYaml(user.ConfigFile(), new_config)
|
||||
}
|
||||
|
||||
func (user User) PostAliases() (map[string]Post, error) {
|
||||
post_aliases := make(map[string]Post)
|
||||
posts, err := user.PublishedPosts()
|
||||
if err != nil {
|
||||
return post_aliases, err
|
||||
}
|
||||
for _, post := range posts {
|
||||
if err != nil {
|
||||
return post_aliases, err
|
||||
}
|
||||
for _, alias := range post.Aliases() {
|
||||
post_aliases[alias] = post
|
||||
}
|
||||
}
|
||||
return post_aliases, nil
|
||||
}
|
||||
|
||||
func (user User) GetPostList(id string) (*PostList, error) {
|
||||
lists := user.Config().Lists
|
||||
|
||||
for _, list := range lists {
|
||||
if list.Id == id {
|
||||
return &list, nil
|
||||
}
|
||||
}
|
||||
|
||||
return &PostList{}, fmt.Errorf("list %s does not exist", id)
|
||||
}
|
||||
|
||||
func (user User) AddPostList(list PostList) error {
|
||||
config := user.Config()
|
||||
config.Lists = append(config.Lists, list)
|
||||
return user.SetConfig(config)
|
||||
}
|
||||
|
||||
func (user User) AddHeaderMenuItem(link MenuItem) error {
|
||||
config := user.Config()
|
||||
config.HeaderMenu = append(config.HeaderMenu, link)
|
||||
return user.SetConfig(config)
|
||||
}
|
||||
|
||||
func (user User) AddFooterMenuItem(link MenuItem) error {
|
||||
config := user.Config()
|
||||
config.FooterMenu = append(config.FooterMenu, link)
|
||||
return user.SetConfig(config)
|
||||
}
|
||||
|
||||
func (user User) ResetPassword(password string) error {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := user.Config()
|
||||
config.PassworHash = string(bytes)
|
||||
return user.SetConfig(config)
|
||||
}
|
||||
|
||||
func (user User) VerifyPassword(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword(
|
||||
[]byte(user.Config().PassworHash), []byte(password),
|
||||
)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (user User) getAuthCodes() []AuthCode {
|
||||
codes := make([]AuthCode, 0)
|
||||
loadFromYaml(user.AuthCodesFile(), &codes)
|
||||
return codes
|
||||
}
|
||||
|
||||
func (user User) addAuthCode(code AuthCode) error {
|
||||
codes := user.getAuthCodes()
|
||||
codes = append(codes, code)
|
||||
return saveToYaml(user.AuthCodesFile(), codes)
|
||||
}
|
||||
|
||||
func (user User) GenerateAuthCode(
|
||||
client_id string, redirect_uri string,
|
||||
code_challenge string, code_challenge_method string,
|
||||
scope string,
|
||||
) (string, error) {
|
||||
// generate code
|
||||
code := GenerateRandomString(32)
|
||||
return code, user.addAuthCode(AuthCode{
|
||||
Code: code,
|
||||
ClientId: client_id,
|
||||
RedirectUri: redirect_uri,
|
||||
CodeChallenge: code_challenge,
|
||||
CodeChallengeMethod: code_challenge_method,
|
||||
Scope: scope,
|
||||
Created: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func (user User) VerifyAuthCode(
|
||||
code string, client_id string, redirect_uri string, code_verifier string,
|
||||
) (bool, AuthCode) {
|
||||
codes := user.getAuthCodes()
|
||||
for _, c := range codes {
|
||||
if c.Code == code && c.ClientId == client_id && c.RedirectUri == redirect_uri {
|
||||
if c.CodeChallengeMethod == "plain" {
|
||||
return c.CodeChallenge == code_verifier, c
|
||||
} else if c.CodeChallengeMethod == "S256" {
|
||||
// hash code_verifier
|
||||
hash := sha256.Sum256([]byte(code_verifier))
|
||||
return c.CodeChallenge == base64.RawURLEncoding.EncodeToString(hash[:]), c
|
||||
} else if c.CodeChallengeMethod == "" {
|
||||
// Check age of code
|
||||
// A maximum lifetime of 10 minutes is recommended ( https://indieauth.spec.indieweb.org/#authorization-response)
|
||||
if time.Since(c.Created) < 10*time.Minute {
|
||||
return true, c
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, AuthCode{}
|
||||
}
|
||||
|
||||
func (user User) getAccessTokens() []AccessToken {
|
||||
codes := make([]AccessToken, 0)
|
||||
loadFromYaml(user.AccessTokensFile(), &codes)
|
||||
return codes
|
||||
}
|
||||
|
||||
func (user User) addAccessToken(code AccessToken) error {
|
||||
codes := user.getAccessTokens()
|
||||
codes = append(codes, code)
|
||||
return saveToYaml(user.AccessTokensFile(), codes)
|
||||
}
|
||||
|
||||
func (user User) GenerateAccessToken(authCode AuthCode) (string, int, error) {
|
||||
// generate code
|
||||
token := GenerateRandomString(32)
|
||||
duration := 24 * 60 * 60
|
||||
return token, duration, user.addAccessToken(AccessToken{
|
||||
Token: token,
|
||||
ClientId: authCode.ClientId,
|
||||
RedirectUri: authCode.RedirectUri,
|
||||
Scope: authCode.Scope,
|
||||
ExpiresIn: duration,
|
||||
Created: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func (user User) ValidateAccessToken(token string) (bool, AccessToken) {
|
||||
tokens := user.getAccessTokens()
|
||||
for _, t := range tokens {
|
||||
if t.Token == token {
|
||||
if time.Since(t.Created) < time.Duration(t.ExpiresIn)*time.Second {
|
||||
return true, t
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, AccessToken{}
|
||||
}
|
||||
|
||||
func (user User) getSessions() []Session {
|
||||
sessions := make([]Session, 0)
|
||||
loadFromYaml(user.SessionsFile(), &sessions)
|
||||
return sessions
|
||||
}
|
||||
|
||||
func (user User) addSession(session Session) error {
|
||||
sessions := user.getSessions()
|
||||
sessions = append(sessions, session)
|
||||
return saveToYaml(user.SessionsFile(), sessions)
|
||||
}
|
||||
|
||||
func (user User) CreateNewSession() string {
|
||||
// generate code
|
||||
code := GenerateRandomString(32)
|
||||
user.addSession(Session{
|
||||
Id: code,
|
||||
Created: time.Now(),
|
||||
ExpiresIn: 30 * 24 * 60 * 60,
|
||||
})
|
||||
return code
|
||||
}
|
||||
|
||||
func (user User) ValidateSession(session_id string) bool {
|
||||
sessions := user.getSessions()
|
||||
for _, session := range sessions {
|
||||
if session.Id == session_id {
|
||||
if time.Since(session.Created) < time.Duration(session.ExpiresIn)*time.Second {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
352
user_test.go
352
user_test.go
|
@ -1,352 +0,0 @@
|
|||
package owl_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"h4kor/owl-blogs"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateNewPostCreatesEntryInPublic(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
// Create a new post
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
files, err := os.ReadDir(path.Join(user.Dir(), "public"))
|
||||
assertions.AssertNoError(t, err, "Error reading directory")
|
||||
assertions.AssertLen(t, files, 1)
|
||||
}
|
||||
|
||||
func TestCreateNewPostCreatesMediaDir(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
// Create a new post
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
_, err := os.Stat(post.MediaDir())
|
||||
assertions.AssertNot(t, os.IsNotExist(err), "Media directory not created")
|
||||
}
|
||||
|
||||
func TestCreateNewPostAddsDateToMetaBlock(t *testing.T) {
|
||||
user := getTestUser()
|
||||
// Create a new post
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
posts, _ := user.PublishedPosts()
|
||||
post, _ := user.GetPost(posts[0].Id())
|
||||
meta := post.Meta()
|
||||
assertions.AssertNot(t, meta.Date.IsZero(), "Date not set")
|
||||
}
|
||||
|
||||
func TestCreateNewPostMultipleCalls(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
// Create a new post
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
files, err := os.ReadDir(path.Join(user.Dir(), "public"))
|
||||
assertions.AssertNoError(t, err, "Error reading directory")
|
||||
assertions.AssertEqual(t, len(files), 3)
|
||||
}
|
||||
|
||||
func TestCanListUserPosts(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
// Create a new post
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
posts, err := user.PublishedPosts()
|
||||
assertions.AssertNoError(t, err, "Error reading posts")
|
||||
assertions.AssertLen(t, posts, 3)
|
||||
}
|
||||
|
||||
func TestCannotListUserPostsInSubdirectories(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
// Create a new post
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
os.Mkdir(path.Join(user.PostDir(), "foo"), 0755)
|
||||
os.Mkdir(path.Join(user.PostDir(), "foo/bar"), 0755)
|
||||
content := ""
|
||||
content += "---\n"
|
||||
content += "title: test\n"
|
||||
content += "---\n"
|
||||
content += "\n"
|
||||
content += "Write your post here.\n"
|
||||
|
||||
os.WriteFile(path.Join(user.PostDir(), "foo/index.md"), []byte(content), 0644)
|
||||
os.WriteFile(path.Join(user.PostDir(), "foo/bar/index.md"), []byte(content), 0644)
|
||||
posts, _ := user.PublishedPosts()
|
||||
postIds := []string{}
|
||||
for _, p := range posts {
|
||||
postIds = append(postIds, p.Id())
|
||||
}
|
||||
if !contains(postIds, "foo") {
|
||||
t.Error("Does not contain post: foo. Found:")
|
||||
for _, p := range posts {
|
||||
t.Error("\t" + p.Id())
|
||||
}
|
||||
}
|
||||
|
||||
if contains(postIds, "foo/bar") {
|
||||
t.Error("Invalid post found: foo/bar. Found:")
|
||||
for _, p := range posts {
|
||||
t.Error("\t" + p.Id())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCannotListUserPostsWithoutIndexMd(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
// Create a new post
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
os.Mkdir(path.Join(user.PostDir(), "foo"), 0755)
|
||||
os.Mkdir(path.Join(user.PostDir(), "foo/bar"), 0755)
|
||||
content := ""
|
||||
content += "---\n"
|
||||
content += "title: test\n"
|
||||
content += "---\n"
|
||||
content += "\n"
|
||||
content += "Write your post here.\n"
|
||||
|
||||
os.WriteFile(path.Join(user.PostDir(), "foo/bar/index.md"), []byte(content), 0644)
|
||||
posts, _ := user.PublishedPosts()
|
||||
postIds := []string{}
|
||||
for _, p := range posts {
|
||||
postIds = append(postIds, p.Id())
|
||||
}
|
||||
if contains(postIds, "foo") {
|
||||
t.Error("Contains invalid post: foo. Found:")
|
||||
for _, p := range posts {
|
||||
t.Error("\t" + p.Id())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUserPostsDoesNotIncludeDrafts(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
// Create a new post
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
content := ""
|
||||
content += "---\n"
|
||||
content += "title: test\n"
|
||||
content += "draft: true\n"
|
||||
content += "---\n"
|
||||
content += "\n"
|
||||
content += "Write your post here.\n"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
posts, _ := user.PublishedPosts()
|
||||
assertions.AssertLen(t, posts, 0)
|
||||
}
|
||||
|
||||
func TestListUsersDraftsExcludedRealWorld(t *testing.T) {
|
||||
// Create a new user
|
||||
repo := getTestRepo(owl.RepoConfig{})
|
||||
user, _ := repo.CreateUser(randomUserName())
|
||||
// Create a new post
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
content := ""
|
||||
content += "---\n"
|
||||
content += "title: Articles September 2019\n"
|
||||
content += "author: h4kor\n"
|
||||
content += "type: post\n"
|
||||
content += "date: -001-11-30T00:00:00+00:00\n"
|
||||
content += "draft: true\n"
|
||||
content += "url: /?p=426\n"
|
||||
content += "categories:\n"
|
||||
content += " - Uncategorised\n"
|
||||
content += "\n"
|
||||
content += "---\n"
|
||||
content += "<https://nesslabs.com/time-anxiety>\n"
|
||||
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
|
||||
posts, _ := user.PublishedPosts()
|
||||
assertions.AssertLen(t, posts, 0)
|
||||
}
|
||||
|
||||
func TestCanLoadPost(t *testing.T) {
|
||||
user := getTestUser()
|
||||
// Create a new post
|
||||
user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
|
||||
posts, _ := user.PublishedPosts()
|
||||
post, _ := user.GetPost(posts[0].Id())
|
||||
assertions.Assert(t, post.Title() == "testpost", "Post title is not correct")
|
||||
}
|
||||
|
||||
func TestUserUrlPath(t *testing.T) {
|
||||
user := getTestUser()
|
||||
assertions.Assert(t, user.UrlPath() == "/user/"+user.Name()+"/", "Wrong url path")
|
||||
}
|
||||
|
||||
func TestUserFullUrl(t *testing.T) {
|
||||
user := getTestUser()
|
||||
assertions.Assert(t, user.FullUrl() == "http://localhost:8080/user/"+user.Name()+"/", "Wrong url path")
|
||||
}
|
||||
|
||||
func TestPostsSortedByPublishingDateLatestFirst(t *testing.T) {
|
||||
user := getTestUser()
|
||||
// Create a new post
|
||||
post1, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
post2, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost2"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test Post\n"
|
||||
content += "date: Wed, 17 Aug 2022 10:50:02 +0000\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post1.ContentFile(), []byte(content), 0644)
|
||||
|
||||
content = "---\n"
|
||||
content += "title: Test Post 2\n"
|
||||
content += "date: Wed, 17 Aug 2022 20:50:06 +0000\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post2.ContentFile(), []byte(content), 0644)
|
||||
|
||||
posts, _ := user.PublishedPosts()
|
||||
assertions.Assert(t, posts[0].Id() == post2.Id(), "Wrong Id")
|
||||
assertions.Assert(t, posts[1].Id() == post1.Id(), "Wrong Id")
|
||||
}
|
||||
|
||||
func TestPostsSortedByPublishingDateLatestFirst2(t *testing.T) {
|
||||
user := getTestUser()
|
||||
// Create a new post
|
||||
posts := []owl.Post{}
|
||||
for i := 59; i >= 0; i-- {
|
||||
post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
content := "---\n"
|
||||
content += "title: Test Post\n"
|
||||
content += fmt.Sprintf("date: Wed, 17 Aug 2022 10:%02d:02 +0000\n", i)
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||
posts = append(posts, post)
|
||||
}
|
||||
|
||||
retPosts, _ := user.PublishedPosts()
|
||||
for i, p := range retPosts {
|
||||
assertions.Assert(t, p.Id() == posts[i].Id(), "Wrong Id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostsSortedByPublishingDateBrokenAtBottom(t *testing.T) {
|
||||
user := getTestUser()
|
||||
// Create a new post
|
||||
post1, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
|
||||
post2, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost2"}, "")
|
||||
|
||||
content := "---\n"
|
||||
content += "title: Test Post\n"
|
||||
content += "date: Wed, 17 +0000\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post1.ContentFile(), []byte(content), 0644)
|
||||
|
||||
content = "---\n"
|
||||
content += "title: Test Post 2\n"
|
||||
content += "date: Wed, 17 Aug 2022 20:50:06 +0000\n"
|
||||
content += "---\n"
|
||||
content += "This is a test"
|
||||
os.WriteFile(post2.ContentFile(), []byte(content), 0644)
|
||||
|
||||
posts, _ := user.PublishedPosts()
|
||||
assertions.Assert(t, posts[0].Id() == post2.Id(), "Wrong Id")
|
||||
assertions.Assert(t, posts[1].Id() == post1.Id(), "Wrong Id")
|
||||
}
|
||||
|
||||
func TestAvatarEmptyIfNotExist(t *testing.T) {
|
||||
user := getTestUser()
|
||||
assertions.Assert(t, user.AvatarUrl() == "", "Avatar should be empty")
|
||||
}
|
||||
|
||||
func TestAvatarSetIfFileExist(t *testing.T) {
|
||||
user := getTestUser()
|
||||
os.WriteFile(path.Join(user.MediaDir(), "avatar.png"), []byte("test"), 0644)
|
||||
assertions.Assert(t, user.AvatarUrl() != "", "Avatar should not be empty")
|
||||
}
|
||||
|
||||
func TestPostNameIllegalFileName(t *testing.T) {
|
||||
user := getTestUser()
|
||||
_, err := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost?///"}, "")
|
||||
assertions.AssertNoError(t, err, "Should not have failed")
|
||||
}
|
||||
|
||||
func TestFaviconIfNotExist(t *testing.T) {
|
||||
user := getTestUser()
|
||||
assertions.Assert(t, user.FaviconUrl() == "", "Favicon should be empty")
|
||||
}
|
||||
|
||||
func TestFaviconSetIfFileExist(t *testing.T) {
|
||||
user := getTestUser()
|
||||
os.WriteFile(path.Join(user.MediaDir(), "favicon.ico"), []byte("test"), 0644)
|
||||
assertions.Assert(t, user.FaviconUrl() != "", "Favicon should not be empty")
|
||||
}
|
||||
|
||||
func TestResetUserPassword(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.ResetPassword("test")
|
||||
assertions.Assert(t, user.Config().PassworHash != "", "Password Hash should not be empty")
|
||||
assertions.Assert(t, user.Config().PassworHash != "test", "Password Hash should not be test")
|
||||
}
|
||||
|
||||
func TestVerifyPassword(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.ResetPassword("test")
|
||||
assertions.Assert(t, user.VerifyPassword("test"), "Password should be correct")
|
||||
assertions.Assert(t, !user.VerifyPassword("test2"), "Password should be incorrect")
|
||||
assertions.Assert(t, !user.VerifyPassword(""), "Password should be incorrect")
|
||||
assertions.Assert(t, !user.VerifyPassword("Test"), "Password should be incorrect")
|
||||
assertions.Assert(t, !user.VerifyPassword("TEST"), "Password should be incorrect")
|
||||
assertions.Assert(t, !user.VerifyPassword("0000000"), "Password should be incorrect")
|
||||
|
||||
}
|
||||
|
||||
func TestValidateAccessTokenWrongToken(t *testing.T) {
|
||||
user := getTestUser()
|
||||
code, _ := user.GenerateAuthCode(
|
||||
"test", "test", "test", "test", "test",
|
||||
)
|
||||
user.GenerateAccessToken(owl.AuthCode{
|
||||
Code: code,
|
||||
ClientId: "test",
|
||||
RedirectUri: "test",
|
||||
CodeChallenge: "test",
|
||||
CodeChallengeMethod: "test",
|
||||
Scope: "test",
|
||||
})
|
||||
valid, _ := user.ValidateAccessToken("test")
|
||||
assertions.Assert(t, !valid, "Token should be invalid")
|
||||
}
|
||||
|
||||
func TestValidateAccessTokenCorrectToken(t *testing.T) {
|
||||
user := getTestUser()
|
||||
code, _ := user.GenerateAuthCode(
|
||||
"test", "test", "test", "test", "test",
|
||||
)
|
||||
token, _, _ := user.GenerateAccessToken(owl.AuthCode{
|
||||
Code: code,
|
||||
ClientId: "test",
|
||||
RedirectUri: "test",
|
||||
CodeChallenge: "test",
|
||||
CodeChallengeMethod: "test",
|
||||
Scope: "test",
|
||||
})
|
||||
valid, aToken := user.ValidateAccessToken(token)
|
||||
assertions.Assert(t, valid, "Token should be valid")
|
||||
assertions.Assert(t, aToken.ClientId == "test", "Token should be valid")
|
||||
assertions.Assert(t, aToken.Token == token, "Token should be valid")
|
||||
}
|
16
utils.go
16
utils.go
|
@ -1,16 +0,0 @@
|
|||
package owl
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func GenerateRandomString(length int) string {
|
||||
chars := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
b := make([]rune, length)
|
||||
for i := range b {
|
||||
k, _ := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
|
||||
b[i] = chars[k.Int64()]
|
||||
}
|
||||
return string(b)
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package owl
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type WebmentionIn struct {
|
||||
Source string `yaml:"source"`
|
||||
Title string `yaml:"title"`
|
||||
ApprovalStatus string `yaml:"approval_status"`
|
||||
RetrievedAt time.Time `yaml:"retrieved_at"`
|
||||
}
|
||||
|
||||
func (webmention *WebmentionIn) UpdateWith(update WebmentionIn) {
|
||||
if update.Title != "" {
|
||||
webmention.Title = update.Title
|
||||
}
|
||||
if update.ApprovalStatus != "" {
|
||||
webmention.ApprovalStatus = update.ApprovalStatus
|
||||
}
|
||||
if !update.RetrievedAt.IsZero() {
|
||||
webmention.RetrievedAt = update.RetrievedAt
|
||||
}
|
||||
}
|
||||
|
||||
type WebmentionOut struct {
|
||||
Target string `yaml:"target"`
|
||||
Supported bool `yaml:"supported"`
|
||||
ScannedAt time.Time `yaml:"scanned_at"`
|
||||
LastSentAt time.Time `yaml:"last_sent_at"`
|
||||
}
|
||||
|
||||
func (webmention *WebmentionOut) UpdateWith(update WebmentionOut) {
|
||||
if update.Supported {
|
||||
webmention.Supported = update.Supported
|
||||
}
|
||||
if !update.ScannedAt.IsZero() {
|
||||
webmention.ScannedAt = update.ScannedAt
|
||||
}
|
||||
if !update.LastSentAt.IsZero() {
|
||||
webmention.LastSentAt = update.LastSentAt
|
||||
}
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
package owl_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"h4kor/owl-blogs"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func constructResponse(html []byte) *http.Response {
|
||||
url, _ := url.Parse("http://example.com/foo/bar")
|
||||
return &http.Response{
|
||||
Request: &http.Request{
|
||||
URL: url,
|
||||
},
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(html))),
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// https://www.w3.org/TR/webmention/#h-webmention-verification
|
||||
//
|
||||
|
||||
func TestParseValidHEntry(t *testing.T) {
|
||||
html := []byte("<div class=\"h-entry\"><div class=\"p-name\">Foo</div></div>")
|
||||
parser := &owl.OwlHtmlParser{}
|
||||
entry, err := parser.ParseHEntry(&http.Response{Body: io.NopCloser(bytes.NewReader(html))})
|
||||
|
||||
assertions.AssertNoError(t, err, "Unable to parse feed")
|
||||
assertions.AssertEqual(t, entry.Title, "Foo")
|
||||
}
|
||||
|
||||
func TestParseValidHEntryWithoutTitle(t *testing.T) {
|
||||
html := []byte("<div class=\"h-entry\"></div><div class=\"p-name\">Foo</div>")
|
||||
parser := &owl.OwlHtmlParser{}
|
||||
entry, err := parser.ParseHEntry(&http.Response{Body: io.NopCloser(bytes.NewReader(html))})
|
||||
|
||||
assertions.AssertNoError(t, err, "Unable to parse feed")
|
||||
assertions.AssertEqual(t, entry.Title, "")
|
||||
}
|
||||
|
||||
func TestGetWebmentionEndpointLink(t *testing.T) {
|
||||
html := []byte("<link rel=\"webmention\" href=\"http://example.com/webmention\" />")
|
||||
parser := &owl.OwlHtmlParser{}
|
||||
endpoint, err := parser.GetWebmentionEndpoint(constructResponse(html))
|
||||
|
||||
assertions.AssertNoError(t, err, "Unable to parse feed")
|
||||
|
||||
assertions.AssertEqual(t, endpoint, "http://example.com/webmention")
|
||||
}
|
||||
|
||||
func TestGetWebmentionEndpointLinkA(t *testing.T) {
|
||||
html := []byte("<a rel=\"webmention\" href=\"http://example.com/webmention\" />")
|
||||
parser := &owl.OwlHtmlParser{}
|
||||
endpoint, err := parser.GetWebmentionEndpoint(constructResponse(html))
|
||||
|
||||
assertions.AssertNoError(t, err, "Unable to parse feed")
|
||||
assertions.AssertEqual(t, endpoint, "http://example.com/webmention")
|
||||
}
|
||||
|
||||
func TestGetWebmentionEndpointLinkAFakeWebmention(t *testing.T) {
|
||||
html := []byte("<a rel=\"not-webmention\" href=\"http://example.com/foo\" /><a rel=\"webmention\" href=\"http://example.com/webmention\" />")
|
||||
parser := &owl.OwlHtmlParser{}
|
||||
endpoint, err := parser.GetWebmentionEndpoint(constructResponse(html))
|
||||
|
||||
assertions.AssertNoError(t, err, "Unable to parse feed")
|
||||
assertions.AssertEqual(t, endpoint, "http://example.com/webmention")
|
||||
}
|
||||
|
||||
func TestGetWebmentionEndpointLinkHeader(t *testing.T) {
|
||||
html := []byte("")
|
||||
parser := &owl.OwlHtmlParser{}
|
||||
resp := constructResponse(html)
|
||||
resp.Header = http.Header{"Link": []string{"<http://example.com/webmention>; rel=\"webmention\""}}
|
||||
endpoint, err := parser.GetWebmentionEndpoint(resp)
|
||||
|
||||
assertions.AssertNoError(t, err, "Unable to parse feed")
|
||||
assertions.AssertEqual(t, endpoint, "http://example.com/webmention")
|
||||
}
|
||||
|
||||
func TestGetWebmentionEndpointLinkHeaderCommas(t *testing.T) {
|
||||
html := []byte("")
|
||||
parser := &owl.OwlHtmlParser{}
|
||||
resp := constructResponse(html)
|
||||
resp.Header = http.Header{
|
||||
"Link": []string{"<https://webmention.rocks/test/19/webmention/error>; rel=\"other\", <https://webmention.rocks/test/19/webmention>; rel=\"webmention\""},
|
||||
}
|
||||
endpoint, err := parser.GetWebmentionEndpoint(resp)
|
||||
|
||||
assertions.AssertNoError(t, err, "Unable to parse feed")
|
||||
assertions.AssertEqual(t, endpoint, "https://webmention.rocks/test/19/webmention")
|
||||
}
|
||||
|
||||
func TestGetWebmentionEndpointRelativeLink(t *testing.T) {
|
||||
html := []byte("<link rel=\"webmention\" href=\"/webmention\" />")
|
||||
parser := &owl.OwlHtmlParser{}
|
||||
endpoint, err := parser.GetWebmentionEndpoint(constructResponse(html))
|
||||
|
||||
assertions.AssertNoError(t, err, "Unable to parse feed")
|
||||
assertions.AssertEqual(t, endpoint, "http://example.com/webmention")
|
||||
}
|
||||
|
||||
func TestGetWebmentionEndpointRelativeLinkInHeader(t *testing.T) {
|
||||
html := []byte("<link rel=\"webmention\" href=\"/webmention\" />")
|
||||
parser := &owl.OwlHtmlParser{}
|
||||
resp := constructResponse(html)
|
||||
resp.Header = http.Header{"Link": []string{"</webmention>; rel=\"webmention\""}}
|
||||
endpoint, err := parser.GetWebmentionEndpoint(resp)
|
||||
|
||||
assertions.AssertNoError(t, err, "Unable to parse feed")
|
||||
assertions.AssertEqual(t, endpoint, "http://example.com/webmention")
|
||||
}
|
||||
|
||||
// func TestRealWorldWebmention(t *testing.T) {
|
||||
// links := []string{
|
||||
// "https://webmention.rocks/test/1",
|
||||
// "https://webmention.rocks/test/2",
|
||||
// "https://webmention.rocks/test/3",
|
||||
// "https://webmention.rocks/test/4",
|
||||
// "https://webmention.rocks/test/5",
|
||||
// "https://webmention.rocks/test/6",
|
||||
// "https://webmention.rocks/test/7",
|
||||
// "https://webmention.rocks/test/8",
|
||||
// "https://webmention.rocks/test/9",
|
||||
// // "https://webmention.rocks/test/10", // not supported
|
||||
// "https://webmention.rocks/test/11",
|
||||
// "https://webmention.rocks/test/12",
|
||||
// "https://webmention.rocks/test/13",
|
||||
// "https://webmention.rocks/test/14",
|
||||
// "https://webmention.rocks/test/15",
|
||||
// "https://webmention.rocks/test/16",
|
||||
// "https://webmention.rocks/test/17",
|
||||
// "https://webmention.rocks/test/18",
|
||||
// "https://webmention.rocks/test/19",
|
||||
// "https://webmention.rocks/test/20",
|
||||
// "https://webmention.rocks/test/21",
|
||||
// "https://webmention.rocks/test/22",
|
||||
// "https://webmention.rocks/test/23/page",
|
||||
// }
|
||||
|
||||
// for _, link := range links {
|
||||
// parser := &owl.OwlHtmlParser{}
|
||||
// client := &owl.OwlHttpClient{}
|
||||
// html, _ := client.Get(link)
|
||||
// _, err := parser.GetWebmentionEndpoint(html)
|
||||
|
||||
// if err != nil {
|
||||
// t.Errorf("Unable to find webmention: %v for link %v", err, link)
|
||||
// }
|
||||
// }
|
||||
|
||||
// }
|
Loading…
Reference in New Issue