commit
2932f2f16c
|
@ -24,3 +24,6 @@ users/
|
||||||
|
|
||||||
.vscode/
|
.vscode/
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
|
|
||||||
|
*.db
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
##
|
##
|
||||||
## Build Container
|
## Build Container
|
||||||
##
|
##
|
||||||
FROM golang:1.19-alpine as build
|
FROM golang:1.20-alpine as build
|
||||||
|
|
||||||
|
|
||||||
RUN apk add --no-cache git
|
RUN apk add --no-cache git
|
||||||
|
@ -21,7 +21,7 @@ RUN go build -o ./out/owl ./cmd/owl
|
||||||
##
|
##
|
||||||
## Run Container
|
## Run Container
|
||||||
##
|
##
|
||||||
FROM alpine:3.9
|
FROM alpine
|
||||||
RUN apk add ca-certificates
|
RUN apk add ca-certificates
|
||||||
|
|
||||||
COPY --from=build /tmp/owl/out/ /bin/
|
COPY --from=build /tmp/owl/out/ /bin/
|
||||||
|
|
114
README.md
114
README.md
|
@ -6,117 +6,3 @@ A simple web server for blogs generated from Markdown files.
|
||||||
|
|
||||||
**_This project is not yet stable. Expect frequent breaking changes! Only use this if you are willing to regularly adjust your project accordingly._**
|
**_This project is not yet stable. Expect frequent breaking changes! Only use this if you are willing to regularly adjust your project accordingly._**
|
||||||
|
|
||||||
## Repository
|
|
||||||
|
|
||||||
A repository holds all data for a web server. It contains multiple users.
|
|
||||||
|
|
||||||
## User
|
|
||||||
|
|
||||||
A user has a collection of posts.
|
|
||||||
Each directory in the `/users/` directory of a repository is considered a user.
|
|
||||||
|
|
||||||
### User Directory structure
|
|
||||||
|
|
||||||
```
|
|
||||||
<user-name>/
|
|
||||||
\- public/
|
|
||||||
\- <post-name>
|
|
||||||
\- index.md
|
|
||||||
-- This will be rendered as the blog post.
|
|
||||||
-- Must be present for the blog post to be valid.
|
|
||||||
-- All other folders will be ignored
|
|
||||||
\- incoming_webmentions.yml
|
|
||||||
-- Used to track incoming webmentions
|
|
||||||
\- outgoing_webmentions.yml
|
|
||||||
-- Used to track outgoing webmentions
|
|
||||||
\- media/
|
|
||||||
-- Contains all media files used in the blog post.
|
|
||||||
-- All files in this folder will be publicly available
|
|
||||||
\- webmention/
|
|
||||||
\- <hash>.yml
|
|
||||||
-- Contains data for a received webmention
|
|
||||||
\- meta/
|
|
||||||
\- base.html
|
|
||||||
-- The template used to render all sites
|
|
||||||
\- config.yml
|
|
||||||
-- Holds information about the user
|
|
||||||
\- VERSION
|
|
||||||
-- Contains the version string.
|
|
||||||
-- Used to determine compatibility in the future
|
|
||||||
\- media/
|
|
||||||
-- All this files will be publicly available. To be used for general files
|
|
||||||
\- avatar.{png|jpg|jpeg|gif}
|
|
||||||
-- Optional: Avatar to be used in various places
|
|
||||||
\- favicon.{png|jpg|jpeg|gif|ico}
|
|
||||||
-- Optional: Favicon for the site
|
|
||||||
```
|
|
||||||
|
|
||||||
### User Config
|
|
||||||
|
|
||||||
Stored in `meta/config.yml`
|
|
||||||
|
|
||||||
```
|
|
||||||
title: "Title of the Blog"
|
|
||||||
subtitle: "Subtitle of the Blog"
|
|
||||||
header_color: "#ff0000"
|
|
||||||
author_name: "Your Name"
|
|
||||||
me:
|
|
||||||
- name: "Connect on Mastodon"
|
|
||||||
url: "https://chaos.social/@h4kor"
|
|
||||||
- name: "I'm on Twitter"
|
|
||||||
url: "https://twitter.com/h4kor"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Post
|
|
||||||
|
|
||||||
Posts are Markdown files with a mandatory metadata head.
|
|
||||||
|
|
||||||
- The `title` will be added to the web page and does not have to be reapeated in the body. It will be used in any lists of posts.
|
|
||||||
- `description` is optional. At the moment this is only used for the HTML head meta data.
|
|
||||||
- `aliases` are optional. They are used as permanent redirects to the actual blog page.
|
|
||||||
- `draft` is false by default. If set to `true` the post will not be accessible.
|
|
||||||
- `reply` optional. Will add the link to the top of the post with `rel="in-reply-to"`. For more infos see: [https://indieweb.org/reply](https://indieweb.org/reply)
|
|
||||||
|
|
||||||
```
|
|
||||||
---
|
|
||||||
title: My new Post
|
|
||||||
Description: Short text used in meta data (and lists in the future)
|
|
||||||
date: 13 Aug 2022 17:07 UTC
|
|
||||||
aliases:
|
|
||||||
- /my/new/post
|
|
||||||
- /old_blog_path/
|
|
||||||
draft: false
|
|
||||||
reply:
|
|
||||||
url: https://link.to/referred_post
|
|
||||||
text: Text used for link
|
|
||||||
---
|
|
||||||
|
|
||||||
Actual post
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Webmentions
|
|
||||||
|
|
||||||
This feature is not yet full supported and needs a lot of manual work. Expect this to change quiet frequently and breaking existing usages.
|
|
||||||
|
|
||||||
To send webmentions use the command `owl webmention`
|
|
||||||
|
|
||||||
Retrieved webmentions have to be approved manually by changing the `approval_status` in the `incoming_webmentions.yml` file.
|
|
||||||
|
|
||||||
#### incoming_webmentions.yml
|
|
||||||
|
|
||||||
```
|
|
||||||
- source: https://example.com/post
|
|
||||||
title: Example Post
|
|
||||||
approval_status: ["", "approved", "rejected"]
|
|
||||||
retrieved_at: 2021-08-13T17:07:00Z
|
|
||||||
```
|
|
||||||
|
|
||||||
#### outgoing_webmentions.yml
|
|
||||||
|
|
||||||
```
|
|
||||||
- target: https://example.com/post
|
|
||||||
supported: true
|
|
||||||
scanned_at: 2021-08-13T17:07:00Z
|
|
||||||
last_sent_at: 2021-08-13T17:07:00Z
|
|
||||||
```
|
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/config"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthorService struct {
|
||||||
|
repo repository.AuthorRepository
|
||||||
|
siteConfigRepo repository.ConfigRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthorService(repo repository.AuthorRepository, siteConfigRepo repository.ConfigRepository) *AuthorService {
|
||||||
|
return &AuthorService{repo: repo, siteConfigRepo: siteConfigRepo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashPassword(password string) (string, error) {
|
||||||
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorService) Create(name string, password string) (*model.Author, error) {
|
||||||
|
hash, err := hashPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s.repo.Create(name, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorService) FindByName(name string) (*model.Author, error) {
|
||||||
|
return s.repo.FindByName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorService) Authenticate(name string, password string) bool {
|
||||||
|
author, err := s.repo.FindByName(name)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
err = bcrypt.CompareHashAndPassword([]byte(author.PasswordHash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorService) getSecretKey() string {
|
||||||
|
siteConfig := model.SiteConfig{}
|
||||||
|
err := s.siteConfigRepo.Get(config.SITE_CONFIG, &siteConfig)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if siteConfig.Secret == "" {
|
||||||
|
siteConfig.Secret = RandStringRunes(64)
|
||||||
|
err = s.siteConfigRepo.Update(config.SITE_CONFIG, siteConfig)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return siteConfig.Secret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorService) CreateToken(name string) (string, error) {
|
||||||
|
hash := sha256.New()
|
||||||
|
_, err := hash.Write([]byte(name + s.getSecretKey()))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s.%x", name, hash.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorService) ValidateToken(token string) (bool, string) {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
witness := parts[len(parts)-1]
|
||||||
|
name := strings.Join(parts[:len(parts)-1], ".")
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
_, err := hash.Write([]byte(name + s.getSecretKey()))
|
||||||
|
if err != nil {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%x", hash.Sum(nil)) == witness, name
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package app_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/app"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"owl-blogs/infra"
|
||||||
|
"owl-blogs/test"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testConfigRepo struct {
|
||||||
|
config model.SiteConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implements repository.SiteConfigRepository.
|
||||||
|
func (c *testConfigRepo) Get(name string, result interface{}) error {
|
||||||
|
*result.(*model.SiteConfig) = c.config
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update implements repository.SiteConfigRepository.
|
||||||
|
func (c *testConfigRepo) Update(name string, result interface{}) error {
|
||||||
|
c.config = result.(model.SiteConfig)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAutherService() *app.AuthorService {
|
||||||
|
db := test.NewMockDb()
|
||||||
|
authorRepo := infra.NewDefaultAuthorRepo(db)
|
||||||
|
authorService := app.NewAuthorService(authorRepo, &testConfigRepo{})
|
||||||
|
return authorService
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorCreate(t *testing.T) {
|
||||||
|
authorService := getAutherService()
|
||||||
|
author, err := authorService.Create("test", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "test", author.Name)
|
||||||
|
require.NotEmpty(t, author.PasswordHash)
|
||||||
|
require.NotEqual(t, "test", author.PasswordHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorFindByName(t *testing.T) {
|
||||||
|
authorService := getAutherService()
|
||||||
|
_, err := authorService.Create("test", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
author, err := authorService.FindByName("test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "test", author.Name)
|
||||||
|
require.NotEmpty(t, author.PasswordHash)
|
||||||
|
require.NotEqual(t, "test", author.PasswordHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorAuthenticate(t *testing.T) {
|
||||||
|
authorService := getAutherService()
|
||||||
|
_, err := authorService.Create("test", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, authorService.Authenticate("test", "test"))
|
||||||
|
require.False(t, authorService.Authenticate("test", "test1"))
|
||||||
|
require.False(t, authorService.Authenticate("test1", "test"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorCreateToken(t *testing.T) {
|
||||||
|
authorService := getAutherService()
|
||||||
|
_, err := authorService.Create("test", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
token, err := authorService.CreateToken("test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, token)
|
||||||
|
require.NotEqual(t, "test", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorValidateToken(t *testing.T) {
|
||||||
|
authorService := getAutherService()
|
||||||
|
_, err := authorService.Create("test", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
token, err := authorService.CreateToken("test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
valid, name := authorService.ValidateToken(token)
|
||||||
|
require.True(t, valid)
|
||||||
|
require.Equal(t, "test", name)
|
||||||
|
valid, _ = authorService.ValidateToken(token[:len(token)-2])
|
||||||
|
require.False(t, valid)
|
||||||
|
valid, _ = authorService.ValidateToken("test")
|
||||||
|
require.False(t, valid)
|
||||||
|
valid, _ = authorService.ValidateToken("test.test")
|
||||||
|
require.False(t, valid)
|
||||||
|
valid, _ = authorService.ValidateToken(strings.Replace(token, "test", "test1", 1))
|
||||||
|
require.False(t, valid)
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BinaryService struct {
|
||||||
|
repo repository.BinaryRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBinaryFileService(repo repository.BinaryRepository) *BinaryService {
|
||||||
|
return &BinaryService{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BinaryService) Create(name string, file []byte) (*model.BinaryFile, error) {
|
||||||
|
return s.repo.Create(name, file, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BinaryService) CreateEntryFile(name string, file []byte, entry model.Entry) (*model.BinaryFile, error) {
|
||||||
|
return s.repo.Create(name, file, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BinaryService) FindById(id string) (*model.BinaryFile, error) {
|
||||||
|
return s.repo.FindById(id)
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EntryTypeRegistry struct {
|
||||||
|
types map[string]model.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEntryTypeRegistry() *EntryTypeRegistry {
|
||||||
|
return &EntryTypeRegistry{types: map[string]model.Entry{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EntryTypeRegistry) entryType(entry model.Entry) string {
|
||||||
|
return reflect.TypeOf(entry).Elem().Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EntryTypeRegistry) Register(entry model.Entry) error {
|
||||||
|
t := r.entryType(entry)
|
||||||
|
if _, ok := r.types[t]; ok {
|
||||||
|
return errors.New("entry type already registered")
|
||||||
|
}
|
||||||
|
r.types[t] = entry
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EntryTypeRegistry) Types() []model.Entry {
|
||||||
|
types := []model.Entry{}
|
||||||
|
for _, t := range r.types {
|
||||||
|
types = append(types, t)
|
||||||
|
}
|
||||||
|
return types
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EntryTypeRegistry) TypeName(entry model.Entry) (string, error) {
|
||||||
|
t := r.entryType(entry)
|
||||||
|
if _, ok := r.types[t]; !ok {
|
||||||
|
return "", errors.New("entry type not registered")
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EntryTypeRegistry) Type(name string) (model.Entry, error) {
|
||||||
|
if _, ok := r.types[name]; !ok {
|
||||||
|
return nil, errors.New("entry type not registered")
|
||||||
|
}
|
||||||
|
|
||||||
|
val := reflect.ValueOf(r.types[name])
|
||||||
|
if val.Kind() == reflect.Ptr {
|
||||||
|
val = reflect.Indirect(val)
|
||||||
|
}
|
||||||
|
newEntry := reflect.New(val.Type()).Interface().(model.Entry)
|
||||||
|
|
||||||
|
return newEntry, nil
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package app_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/app"
|
||||||
|
"owl-blogs/test"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRegistryTypeNameNotExisting(t *testing.T) {
|
||||||
|
register := app.NewEntryTypeRegistry()
|
||||||
|
_, err := register.TypeName(&test.MockEntry{})
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryTypeName(t *testing.T) {
|
||||||
|
register := app.NewEntryTypeRegistry()
|
||||||
|
register.Register(&test.MockEntry{})
|
||||||
|
name, err := register.TypeName(&test.MockEntry{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "MockEntry", name)
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EntryService struct {
|
||||||
|
EntryRepository repository.EntryRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEntryService(entryRepository repository.EntryRepository) *EntryService {
|
||||||
|
return &EntryService{EntryRepository: entryRepository}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EntryService) Create(entry model.Entry) error {
|
||||||
|
return s.EntryRepository.Create(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EntryService) Update(entry model.Entry) error {
|
||||||
|
return s.EntryRepository.Update(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EntryService) Delete(entry model.Entry) error {
|
||||||
|
return s.EntryRepository.Delete(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EntryService) FindById(id string) (model.Entry, error) {
|
||||||
|
return s.EntryRepository.FindById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EntryService) filterEntries(entries []model.Entry, published bool, drafts bool) []model.Entry {
|
||||||
|
filteredEntries := make([]model.Entry, 0)
|
||||||
|
for _, entry := range entries {
|
||||||
|
if published && entry.PublishedAt() != nil && !entry.PublishedAt().IsZero() {
|
||||||
|
filteredEntries = append(filteredEntries, entry)
|
||||||
|
}
|
||||||
|
if drafts && (entry.PublishedAt() == nil || entry.PublishedAt().IsZero()) {
|
||||||
|
filteredEntries = append(filteredEntries, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EntryService) FindAllByType(types *[]string, published bool, drafts bool) ([]model.Entry, error) {
|
||||||
|
entries, err := s.EntryRepository.FindAll(types)
|
||||||
|
return s.filterEntries(entries, published, drafts), err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EntryService) FindAll() ([]model.Entry, error) {
|
||||||
|
entries, err := s.EntryRepository.FindAll(nil)
|
||||||
|
return s.filterEntries(entries, true, true), err
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EntryRepository interface {
|
||||||
|
Create(entry model.Entry) error
|
||||||
|
Update(entry model.Entry) error
|
||||||
|
Delete(entry model.Entry) error
|
||||||
|
FindById(id string) (model.Entry, error)
|
||||||
|
FindAll(types *[]string) ([]model.Entry, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BinaryRepository interface {
|
||||||
|
// Create creates a new binary file
|
||||||
|
// The name is the original file name, and is not unique
|
||||||
|
// BinaryFile.Id is a unique identifier
|
||||||
|
Create(name string, data []byte, entry model.Entry) (*model.BinaryFile, error)
|
||||||
|
FindById(id string) (*model.BinaryFile, error)
|
||||||
|
FindByNameForEntry(name string, entry model.Entry) (*model.BinaryFile, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthorRepository interface {
|
||||||
|
// Create creates a new author
|
||||||
|
// It returns an error if the name is already taken
|
||||||
|
Create(name string, passwordHash string) (*model.Author, error)
|
||||||
|
// FindByName finds an author by name
|
||||||
|
// It returns an error if the author is not found
|
||||||
|
FindByName(name string) (*model.Author, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigRepository interface {
|
||||||
|
Get(name string, config interface{}) error
|
||||||
|
Update(name string, siteConfig interface{}) error
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
|
|
||||||
|
func RandStringRunes(n int) string {
|
||||||
|
b := make([]rune, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
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")
|
|
||||||
}
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/infra"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var user string
|
||||||
|
var password string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(newAuthorCmd)
|
||||||
|
|
||||||
|
newAuthorCmd.Flags().StringVarP(&user, "user", "u", "", "The user name")
|
||||||
|
newAuthorCmd.MarkFlagRequired("user")
|
||||||
|
newAuthorCmd.Flags().StringVarP(&password, "password", "p", "", "The password")
|
||||||
|
newAuthorCmd.MarkFlagRequired("password")
|
||||||
|
}
|
||||||
|
|
||||||
|
var newAuthorCmd = &cobra.Command{
|
||||||
|
Use: "new-author",
|
||||||
|
Short: "Creates a new author",
|
||||||
|
Long: `Creates a new author`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db := infra.NewSqliteDB(DbPath)
|
||||||
|
App(db).AuthorService.Create(user, password)
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"owl-blogs/app"
|
||||||
|
entrytypes "owl-blogs/entry_types"
|
||||||
|
"owl-blogs/infra"
|
||||||
|
"owl-blogs/test"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getUserToken(service *app.AuthorService) string {
|
||||||
|
_, err := service.Create("test", "test")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
token, err := service.CreateToken("test")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEditorFormGet(t *testing.T) {
|
||||||
|
db := test.NewMockDb()
|
||||||
|
owlApp := App(db)
|
||||||
|
app := owlApp.FiberApp
|
||||||
|
token := getUserToken(owlApp.AuthorService)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/editor/Image", nil)
|
||||||
|
req.AddCookie(&http.Cookie{Name: "token", Value: token})
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEditorFormGetNoAuth(t *testing.T) {
|
||||||
|
db := test.NewMockDb()
|
||||||
|
owlApp := App(db)
|
||||||
|
app := owlApp.FiberApp
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/editor/Image", nil)
|
||||||
|
req.AddCookie(&http.Cookie{Name: "token", Value: "invalid"})
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 302, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEditorFormPost(t *testing.T) {
|
||||||
|
db := test.NewMockDb()
|
||||||
|
owlApp := App(db)
|
||||||
|
app := owlApp.FiberApp
|
||||||
|
token := getUserToken(owlApp.AuthorService)
|
||||||
|
repo := infra.NewEntryRepository(db, owlApp.Registry)
|
||||||
|
binRepo := infra.NewBinaryFileRepo(db)
|
||||||
|
|
||||||
|
fileDir, _ := os.Getwd()
|
||||||
|
fileName := "../../test/fixtures/test.png"
|
||||||
|
filePath := path.Join(fileDir, fileName)
|
||||||
|
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
fileBytes, err := ioutil.ReadFile(filePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
part, _ := writer.CreateFormFile("ImageId", filepath.Base(file.Name()))
|
||||||
|
io.Copy(part, file)
|
||||||
|
part, _ = writer.CreateFormField("Content")
|
||||||
|
io.WriteString(part, "test content")
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/editor/Image", body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
req.AddCookie(&http.Cookie{Name: "token", Value: token})
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 302, resp.StatusCode)
|
||||||
|
require.Contains(t, resp.Header.Get("Location"), "/posts/")
|
||||||
|
|
||||||
|
id := strings.Split(resp.Header.Get("Location"), "/")[2]
|
||||||
|
entry, err := repo.FindById(id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "test content", entry.MetaData().(*entrytypes.ImageMetaData).Content)
|
||||||
|
imageId := entry.MetaData().(*entrytypes.ImageMetaData).ImageId
|
||||||
|
require.NotZero(t, imageId)
|
||||||
|
bin, err := binRepo.FindById(imageId)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, bin.Name, "test.png")
|
||||||
|
require.Equal(t, fileBytes, bin.Data)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEditorFormPostNoAuth(t *testing.T) {
|
||||||
|
db := test.NewMockDb()
|
||||||
|
owlApp := App(db)
|
||||||
|
app := owlApp.FiberApp
|
||||||
|
|
||||||
|
fileDir, _ := os.Getwd()
|
||||||
|
fileName := "../../test/fixtures/test.png"
|
||||||
|
filePath := path.Join(fileDir, fileName)
|
||||||
|
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
part, _ := writer.CreateFormFile("ImageId", filepath.Base(file.Name()))
|
||||||
|
io.Copy(part, file)
|
||||||
|
part, _ = writer.CreateFormField("Content")
|
||||||
|
io.WriteString(part, "test content")
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/editor/Image", body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
req.AddCookie(&http.Cookie{Name: "token", Value: "invalid"})
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 302, resp.StatusCode)
|
||||||
|
require.Contains(t, resp.Header.Get("Location"), "/auth/login")
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"owl-blogs/config"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
entrytypes "owl-blogs/entry_types"
|
||||||
|
"owl-blogs/importer"
|
||||||
|
"owl-blogs/infra"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var userPath string
|
||||||
|
var author string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(importCmd)
|
||||||
|
|
||||||
|
importCmd.Flags().StringVarP(&userPath, "path", "p", "", "Path to the user folder")
|
||||||
|
importCmd.MarkFlagRequired("path")
|
||||||
|
importCmd.Flags().StringVarP(&author, "author", "a", "", "The author name")
|
||||||
|
importCmd.MarkFlagRequired("author")
|
||||||
|
}
|
||||||
|
|
||||||
|
var importCmd = &cobra.Command{
|
||||||
|
Use: "import",
|
||||||
|
Short: "Import data from v1",
|
||||||
|
Long: `Import data from v1`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db := infra.NewSqliteDB(DbPath)
|
||||||
|
app := App(db)
|
||||||
|
|
||||||
|
posts, err := importer.AllUserPosts(userPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// import config
|
||||||
|
bytes, err := os.ReadFile(path.Join(userPath, "meta/config.yml"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
v1Config := importer.V1UserConfig{}
|
||||||
|
yaml.Unmarshal(bytes, &v1Config)
|
||||||
|
|
||||||
|
mes := []model.MeLinks{}
|
||||||
|
for _, me := range v1Config.Me {
|
||||||
|
mes = append(mes, model.MeLinks{
|
||||||
|
Name: me.Name,
|
||||||
|
Url: me.Url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
lists := []model.EntryList{}
|
||||||
|
for _, list := range v1Config.Lists {
|
||||||
|
lists = append(lists, model.EntryList{
|
||||||
|
Id: list.Id,
|
||||||
|
Title: list.Title,
|
||||||
|
Include: importer.ConvertTypeList(list.Include, app.Registry),
|
||||||
|
ListType: list.ListType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
headerMenu := []model.MenuItem{}
|
||||||
|
for _, item := range v1Config.HeaderMenu {
|
||||||
|
headerMenu = append(headerMenu, model.MenuItem{
|
||||||
|
Title: item.Title,
|
||||||
|
List: item.List,
|
||||||
|
Url: item.Url,
|
||||||
|
Post: item.Post,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
footerMenu := []model.MenuItem{}
|
||||||
|
for _, item := range v1Config.FooterMenu {
|
||||||
|
footerMenu = append(footerMenu, model.MenuItem{
|
||||||
|
Title: item.Title,
|
||||||
|
List: item.List,
|
||||||
|
Url: item.Url,
|
||||||
|
Post: item.Post,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
v2Config := &model.SiteConfig{}
|
||||||
|
err = app.SiteConfigRepo.Get(config.SITE_CONFIG, v2Config)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
v2Config.Title = v1Config.Title
|
||||||
|
v2Config.SubTitle = v1Config.SubTitle
|
||||||
|
v2Config.HeaderColor = v1Config.HeaderColor
|
||||||
|
v2Config.AuthorName = v1Config.AuthorName
|
||||||
|
v2Config.Me = mes
|
||||||
|
v2Config.Lists = lists
|
||||||
|
v2Config.PrimaryListInclude = importer.ConvertTypeList(v1Config.PrimaryListInclude, app.Registry)
|
||||||
|
v2Config.HeaderMenu = headerMenu
|
||||||
|
v2Config.FooterMenu = footerMenu
|
||||||
|
|
||||||
|
err = app.SiteConfigRepo.Update(config.SITE_CONFIG, v2Config)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, post := range posts {
|
||||||
|
existing, _ := app.EntryService.FindById(post.Id)
|
||||||
|
if existing != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Println(post.Meta.Type)
|
||||||
|
|
||||||
|
// import assets
|
||||||
|
mediaDir := path.Join(userPath, post.MediaDir())
|
||||||
|
println(mediaDir)
|
||||||
|
files := importer.ListDir(mediaDir)
|
||||||
|
for _, file := range files {
|
||||||
|
// mock entry to pass to binary service
|
||||||
|
entry := &entrytypes.Article{}
|
||||||
|
entry.SetID(post.Id)
|
||||||
|
|
||||||
|
fileData, err := os.ReadFile(path.Join(mediaDir, file))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
app.BinaryService.CreateEntryFile(file, fileData, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry model.Entry
|
||||||
|
|
||||||
|
switch post.Meta.Type {
|
||||||
|
case "article":
|
||||||
|
entry = &entrytypes.Article{}
|
||||||
|
entry.SetID(post.Id)
|
||||||
|
entry.SetPublishedAt(&post.Meta.Date)
|
||||||
|
entry.SetMetaData(&entrytypes.ArticleMetaData{
|
||||||
|
Title: post.Meta.Title,
|
||||||
|
Content: post.Content,
|
||||||
|
})
|
||||||
|
case "bookmark":
|
||||||
|
entry = &entrytypes.Bookmark{}
|
||||||
|
entry.SetID(post.Id)
|
||||||
|
entry.SetPublishedAt(&post.Meta.Date)
|
||||||
|
entry.SetMetaData(&entrytypes.BookmarkMetaData{
|
||||||
|
Url: post.Meta.Bookmark.Url,
|
||||||
|
Title: post.Meta.Bookmark.Text,
|
||||||
|
Content: post.Content,
|
||||||
|
})
|
||||||
|
case "reply":
|
||||||
|
entry = &entrytypes.Reply{}
|
||||||
|
entry.SetID(post.Id)
|
||||||
|
entry.SetPublishedAt(&post.Meta.Date)
|
||||||
|
entry.SetMetaData(&entrytypes.ReplyMetaData{
|
||||||
|
Url: post.Meta.Reply.Url,
|
||||||
|
Title: post.Meta.Reply.Text,
|
||||||
|
Content: post.Content,
|
||||||
|
})
|
||||||
|
case "photo":
|
||||||
|
entry = &entrytypes.Image{}
|
||||||
|
entry.SetID(post.Id)
|
||||||
|
entry.SetPublishedAt(&post.Meta.Date)
|
||||||
|
entry.SetMetaData(&entrytypes.ImageMetaData{
|
||||||
|
Title: post.Meta.Title,
|
||||||
|
Content: post.Content,
|
||||||
|
ImageId: post.Meta.PhotoPath,
|
||||||
|
})
|
||||||
|
case "note":
|
||||||
|
entry = &entrytypes.Note{}
|
||||||
|
entry.SetID(post.Id)
|
||||||
|
entry.SetPublishedAt(&post.Meta.Date)
|
||||||
|
entry.SetMetaData(&entrytypes.NoteMetaData{
|
||||||
|
Content: post.Content,
|
||||||
|
})
|
||||||
|
case "recipe":
|
||||||
|
entry = &entrytypes.Recipe{}
|
||||||
|
entry.SetID(post.Id)
|
||||||
|
entry.SetPublishedAt(&post.Meta.Date)
|
||||||
|
entry.SetMetaData(&entrytypes.RecipeMetaData{
|
||||||
|
Title: post.Meta.Title,
|
||||||
|
Yield: post.Meta.Recipe.Yield,
|
||||||
|
Duration: post.Meta.Recipe.Duration,
|
||||||
|
Ingredients: post.Meta.Recipe.Ingredients,
|
||||||
|
Content: post.Content,
|
||||||
|
})
|
||||||
|
case "page":
|
||||||
|
entry = &entrytypes.Page{}
|
||||||
|
entry.SetID(post.Id)
|
||||||
|
entry.SetPublishedAt(&post.Meta.Date)
|
||||||
|
entry.SetMetaData(&entrytypes.PageMetaData{
|
||||||
|
Title: post.Meta.Title,
|
||||||
|
Content: post.Content,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
panic("Unknown type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry != nil {
|
||||||
|
entry.SetAuthorId(author)
|
||||||
|
app.EntryService.Create(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -3,11 +3,16 @@ package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"owl-blogs/app"
|
||||||
|
entrytypes "owl-blogs/entry_types"
|
||||||
|
"owl-blogs/infra"
|
||||||
|
"owl-blogs/web"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var repoPath string
|
const DbPath = "owlblogs.db"
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "owl",
|
Use: "owl",
|
||||||
Short: "Owl Blogs is a not so static blog generator",
|
Short: "Owl Blogs is a not so static blog generator",
|
||||||
|
@ -20,10 +25,26 @@ func Execute() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func App(db infra.Database) *web.WebApp {
|
||||||
|
registry := app.NewEntryTypeRegistry()
|
||||||
|
registry.Register(&entrytypes.Image{})
|
||||||
|
registry.Register(&entrytypes.Article{})
|
||||||
|
registry.Register(&entrytypes.Page{})
|
||||||
|
registry.Register(&entrytypes.Recipe{})
|
||||||
|
registry.Register(&entrytypes.Note{})
|
||||||
|
registry.Register(&entrytypes.Bookmark{})
|
||||||
|
registry.Register(&entrytypes.Reply{})
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringVar(&repoPath, "repo", ".", "Path to the repository to use.")
|
entryRepo := infra.NewEntryRepository(db, registry)
|
||||||
rootCmd.PersistentFlags().StringVar(&user, "user", "", "Username. Required for some commands.")
|
binRepo := infra.NewBinaryFileRepo(db)
|
||||||
|
authorRepo := infra.NewDefaultAuthorRepo(db)
|
||||||
|
siteConfigRepo := infra.NewConfigRepo(db)
|
||||||
|
|
||||||
|
entryService := app.NewEntryService(entryRepo)
|
||||||
|
binaryService := app.NewBinaryFileService(binRepo)
|
||||||
|
authorService := app.NewAuthorService(authorRepo, siteConfigRepo)
|
||||||
|
|
||||||
|
return web.NewWebApp(entryService, registry, binaryService, authorService, siteConfigRepo)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"owl-blogs/test"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMediaWithSpace(t *testing.T) {
|
||||||
|
db := test.NewMockDb()
|
||||||
|
owlApp := App(db)
|
||||||
|
app := owlApp.FiberApp
|
||||||
|
|
||||||
|
_, err := owlApp.BinaryService.Create("name with space.jpg", []byte("111"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/media/name%20with%20space.jpg", nil)
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
|
}
|
|
@ -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,17 +1,13 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
web "h4kor/owl-blogs/cmd/owl/web"
|
"owl-blogs/infra"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var port int
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(webCmd)
|
rootCmd.AddCommand(webCmd)
|
||||||
|
|
||||||
webCmd.PersistentFlags().IntVar(&port, "port", 8080, "Port to use")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var webCmd = &cobra.Command{
|
var webCmd = &cobra.Command{
|
||||||
|
@ -19,6 +15,7 @@ var webCmd = &cobra.Command{
|
||||||
Short: "Start the web server",
|
Short: "Start the web server",
|
||||||
Long: `Start the web server`,
|
Long: `Start the web server`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
web.StartServer(repoPath, port)
|
db := infra.NewSqliteDB(DbPath)
|
||||||
|
App(db).Run()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
const (
|
||||||
|
SITE_CONFIG = "site_config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config interface {
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnvConfig struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvOrPanic(key string) string {
|
||||||
|
value, set := os.LookupEnv(key)
|
||||||
|
if !set {
|
||||||
|
panic("Environment variable " + key + " is not set")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig() Config {
|
||||||
|
return &EnvConfig{}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type Author struct {
|
||||||
|
Name string
|
||||||
|
PasswordHash string
|
||||||
|
FullUrl string
|
||||||
|
AvatarUrl string
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BinaryFile struct {
|
||||||
|
Id string
|
||||||
|
Name string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BinaryFile) Mime() string {
|
||||||
|
parts := strings.Split(b.Name, ".")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
t := mime.TypeByExtension("." + parts[len(parts)-1])
|
||||||
|
if t == "" {
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package model_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMimeType(t *testing.T) {
|
||||||
|
bin := model.BinaryFile{Name: "test.jpg"}
|
||||||
|
require.Equal(t, "image/jpeg", bin.Mime())
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type EntryContent string
|
||||||
|
|
||||||
|
type Entry interface {
|
||||||
|
ID() string
|
||||||
|
Content() EntryContent
|
||||||
|
PublishedAt() *time.Time
|
||||||
|
AuthorId() string
|
||||||
|
MetaData() interface{}
|
||||||
|
|
||||||
|
// Optional: can return empty string
|
||||||
|
Title() string
|
||||||
|
|
||||||
|
SetID(id string)
|
||||||
|
SetPublishedAt(publishedAt *time.Time)
|
||||||
|
SetMetaData(metaData interface{})
|
||||||
|
SetAuthorId(authorId string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EntryMetaData interface {
|
||||||
|
}
|
||||||
|
|
||||||
|
type EntryBase struct {
|
||||||
|
id string
|
||||||
|
publishedAt *time.Time
|
||||||
|
authorId string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EntryBase) ID() string {
|
||||||
|
return e.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EntryBase) PublishedAt() *time.Time {
|
||||||
|
return e.publishedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EntryBase) SetID(id string) {
|
||||||
|
e.id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EntryBase) SetPublishedAt(publishedAt *time.Time) {
|
||||||
|
e.publishedAt = publishedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EntryBase) AuthorId() string {
|
||||||
|
return e.authorId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EntryBase) SetAuthorId(authorId string) {
|
||||||
|
e.authorId = authorId
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type MeLinks struct {
|
||||||
|
Name string
|
||||||
|
Url string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EntryList struct {
|
||||||
|
Id string
|
||||||
|
Title string
|
||||||
|
Include []string
|
||||||
|
ListType string
|
||||||
|
}
|
||||||
|
|
||||||
|
type MenuItem struct {
|
||||||
|
Title string
|
||||||
|
List string
|
||||||
|
Url string
|
||||||
|
Post string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SiteConfig struct {
|
||||||
|
Title string
|
||||||
|
SubTitle string
|
||||||
|
HeaderColor string
|
||||||
|
AuthorName string
|
||||||
|
Me []MeLinks
|
||||||
|
Lists []EntryList
|
||||||
|
PrimaryListInclude []string
|
||||||
|
HeaderMenu []MenuItem
|
||||||
|
FooterMenu []MenuItem
|
||||||
|
Secret string
|
||||||
|
AvatarUrl string
|
||||||
|
}
|
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}}
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
package entrytypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"owl-blogs/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Article struct {
|
||||||
|
model.EntryBase
|
||||||
|
meta ArticleMetaData
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArticleMetaData struct {
|
||||||
|
Title string `owl:"inputType=text"`
|
||||||
|
Content string `owl:"inputType=text widget=textarea"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Article) Title() string {
|
||||||
|
return e.meta.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Article) Content() model.EntryContent {
|
||||||
|
str, err := render.RenderTemplateToString("entry/Article", e)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return model.EntryContent(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Article) MetaData() interface{} {
|
||||||
|
return &e.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Article) SetMetaData(metaData interface{}) {
|
||||||
|
e.meta = *metaData.(*ArticleMetaData)
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package entrytypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"owl-blogs/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bookmark struct {
|
||||||
|
model.EntryBase
|
||||||
|
meta BookmarkMetaData
|
||||||
|
}
|
||||||
|
|
||||||
|
type BookmarkMetaData struct {
|
||||||
|
Title string `owl:"inputType=text"`
|
||||||
|
Url string `owl:"inputType=text"`
|
||||||
|
Content string `owl:"inputType=text widget=textarea"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Bookmark) Title() string {
|
||||||
|
return e.meta.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Bookmark) Content() model.EntryContent {
|
||||||
|
str, err := render.RenderTemplateToString("entry/Bookmark", e)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return model.EntryContent(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Bookmark) MetaData() interface{} {
|
||||||
|
return &e.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Bookmark) SetMetaData(metaData interface{}) {
|
||||||
|
e.meta = *metaData.(*BookmarkMetaData)
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package entrytypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"owl-blogs/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Image struct {
|
||||||
|
model.EntryBase
|
||||||
|
meta ImageMetaData
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageMetaData struct {
|
||||||
|
ImageId string `owl:"inputType=file"`
|
||||||
|
Title string `owl:"inputType=text"`
|
||||||
|
Content string `owl:"inputType=text widget=textarea"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Image) Title() string {
|
||||||
|
return e.meta.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Image) Content() model.EntryContent {
|
||||||
|
str, err := render.RenderTemplateToString("entry/Image", e)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return model.EntryContent(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Image) MetaData() interface{} {
|
||||||
|
return &e.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Image) SetMetaData(metaData interface{}) {
|
||||||
|
e.meta = *metaData.(*ImageMetaData)
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package entrytypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"owl-blogs/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Note struct {
|
||||||
|
model.EntryBase
|
||||||
|
meta NoteMetaData
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoteMetaData struct {
|
||||||
|
Content string `owl:"inputType=text widget=textarea"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Note) Title() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Note) Content() model.EntryContent {
|
||||||
|
str, err := render.RenderTemplateToString("entry/Note", e)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return model.EntryContent(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Note) MetaData() interface{} {
|
||||||
|
return &e.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Note) SetMetaData(metaData interface{}) {
|
||||||
|
e.meta = *metaData.(*NoteMetaData)
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package entrytypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"owl-blogs/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Page struct {
|
||||||
|
model.EntryBase
|
||||||
|
meta PageMetaData
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageMetaData struct {
|
||||||
|
Title string `owl:"inputType=text"`
|
||||||
|
Content string `owl:"inputType=text widget=textarea"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Page) Title() string {
|
||||||
|
return e.meta.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Page) Content() model.EntryContent {
|
||||||
|
str, err := render.RenderTemplateToString("entry/Page", e)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return model.EntryContent(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Page) MetaData() interface{} {
|
||||||
|
return &e.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Page) SetMetaData(metaData interface{}) {
|
||||||
|
e.meta = *metaData.(*PageMetaData)
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package entrytypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"owl-blogs/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Recipe struct {
|
||||||
|
model.EntryBase
|
||||||
|
meta RecipeMetaData
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecipeMetaData struct {
|
||||||
|
Title string `owl:"inputType=text"`
|
||||||
|
Yield string `owl:"inputType=text"`
|
||||||
|
Duration string `owl:"inputType=text"`
|
||||||
|
Ingredients []string `owl:"inputType=text widget=textarea"`
|
||||||
|
Content string `owl:"inputType=text widget=textarea"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Recipe) Title() string {
|
||||||
|
return e.meta.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Recipe) Content() model.EntryContent {
|
||||||
|
str, err := render.RenderTemplateToString("entry/Recipe", e)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return model.EntryContent(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Recipe) MetaData() interface{} {
|
||||||
|
return &e.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Recipe) SetMetaData(metaData interface{}) {
|
||||||
|
e.meta = *metaData.(*RecipeMetaData)
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package entrytypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"owl-blogs/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Reply struct {
|
||||||
|
model.EntryBase
|
||||||
|
meta ReplyMetaData
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReplyMetaData struct {
|
||||||
|
Title string `owl:"inputType=text"`
|
||||||
|
Url string `owl:"inputType=text"`
|
||||||
|
Content string `owl:"inputType=text widget=textarea"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Reply) Title() string {
|
||||||
|
return "Re: " + e.meta.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Reply) Content() model.EntryContent {
|
||||||
|
str, err := render.RenderTemplateToString("entry/Reply", e)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return model.EntryContent(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Reply) MetaData() interface{} {
|
||||||
|
return &e.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Reply) SetMetaData(metaData interface{}) {
|
||||||
|
e.meta = *metaData.(*ReplyMetaData)
|
||||||
|
}
|
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 |
42
go.mod
42
go.mod
|
@ -1,17 +1,35 @@
|
||||||
module h4kor/owl-blogs
|
module owl-blogs
|
||||||
|
|
||||||
go 1.18
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/julienschmidt/httprouter v1.3.0
|
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||||
github.com/spf13/cobra v1.5.0
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/yuin/goldmark v1.4.13
|
github.com/gofiber/fiber/v2 v2.47.0 // indirect
|
||||||
golang.org/x/net v0.1.0
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
)
|
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||||
|
github.com/klauspost/compress v1.16.3 // indirect
|
||||||
require (
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||||
|
github.com/philhofer/fwd v1.1.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
|
||||||
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||||
|
github.com/spf13/cobra v1.7.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
golang.org/x/crypto v0.1.0 // indirect
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.8.4 // indirect
|
||||||
|
github.com/tinylib/msgp v1.1.8 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.47.0 // indirect
|
||||||
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
github.com/yuin/goldmark v1.5.4 // indirect
|
||||||
|
golang.org/x/crypto v0.11.0 // indirect
|
||||||
|
golang.org/x/sys v0.10.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
127
go.sum
127
go.sum
|
@ -1,23 +1,120 @@
|
||||||
|
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||||
|
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
github.com/gofiber/fiber/v2 v2.47.0 h1:EN5lHVCc+Pyqh5OEsk8fzRiifgwpbrP0rulQ4iNf3fs=
|
||||||
|
github.com/gofiber/fiber/v2 v2.47.0/go.mod h1:mbFMVN1lQuzziTkkakgtKKdjfsXSw9BKR5lmcNksUoU=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||||
|
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||||
|
github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
|
||||||
|
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||||
|
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||||
|
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
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/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
|
||||||
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
|
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
|
||||||
|
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
|
||||||
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||||
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||||
|
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||||
|
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
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/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
|
||||||
|
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||||
|
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
|
||||||
|
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||||
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
|
||||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||||
|
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||||
|
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||||
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
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
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package importer
|
||||||
|
|
||||||
|
type V1UserConfig struct {
|
||||||
|
Title string `yaml:"title"`
|
||||||
|
SubTitle string `yaml:"subtitle"`
|
||||||
|
HeaderColor string `yaml:"header_color"`
|
||||||
|
AuthorName string `yaml:"author_name"`
|
||||||
|
Me []V1UserMe `yaml:"me"`
|
||||||
|
PassworHash string `yaml:"password_hash"`
|
||||||
|
Lists []V1PostList `yaml:"lists"`
|
||||||
|
PrimaryListInclude []string `yaml:"primary_list_include"`
|
||||||
|
HeaderMenu []V1MenuItem `yaml:"header_menu"`
|
||||||
|
FooterMenu []V1MenuItem `yaml:"footer_menu"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type V1UserMe struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Url string `yaml:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type V1PostList struct {
|
||||||
|
Id string `yaml:"id"`
|
||||||
|
Title string `yaml:"title"`
|
||||||
|
Include []string `yaml:"include"`
|
||||||
|
ListType string `yaml:"list_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type V1MenuItem struct {
|
||||||
|
Title string `yaml:"title"`
|
||||||
|
List string `yaml:"list"`
|
||||||
|
Url string `yaml:"url"`
|
||||||
|
Post string `yaml:"post"`
|
||||||
|
}
|
|
@ -0,0 +1,238 @@
|
||||||
|
package importer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"owl-blogs/app"
|
||||||
|
entrytypes "owl-blogs/entry_types"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Post struct {
|
||||||
|
Id string
|
||||||
|
Meta PostMeta
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (post *Post) MediaDir() string {
|
||||||
|
return path.Join("public", post.Id, "media")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadContent(data []byte) string {
|
||||||
|
|
||||||
|
// 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:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadMeta(data []byte) (PostMeta, error) {
|
||||||
|
|
||||||
|
// 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 PostMeta{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AllUserPosts(userPath string) ([]Post, error) {
|
||||||
|
postFiles := ListDir(path.Join(userPath, "public"))
|
||||||
|
posts := make([]Post, 0)
|
||||||
|
for _, id := range postFiles {
|
||||||
|
// if is a directory and has index.md, add to posts
|
||||||
|
if dirExists(path.Join(userPath, "public", id)) {
|
||||||
|
if fileExists(path.Join(userPath, "public", id, "index.md")) {
|
||||||
|
postData, err := os.ReadFile(path.Join(userPath, "public", id, "index.md"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
meta, err := LoadMeta(postData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
post := Post{
|
||||||
|
Id: id,
|
||||||
|
Content: LoadContent(postData),
|
||||||
|
Meta: meta,
|
||||||
|
}
|
||||||
|
posts = append(posts, post)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return posts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListDir(path string) []string {
|
||||||
|
dir, _ := os.Open(path)
|
||||||
|
defer dir.Close()
|
||||||
|
files, _ := dir.Readdirnames(-1)
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
func dirExists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileExists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertTypeList(v1 []string, registry *app.EntryTypeRegistry) []string {
|
||||||
|
v2 := make([]string, len(v1))
|
||||||
|
for i, v1Type := range v1 {
|
||||||
|
switch v1Type {
|
||||||
|
case "article":
|
||||||
|
name, _ := registry.TypeName(&entrytypes.Article{})
|
||||||
|
v2[i] = name
|
||||||
|
case "bookmark":
|
||||||
|
name, _ := registry.TypeName(&entrytypes.Bookmark{})
|
||||||
|
v2[i] = name
|
||||||
|
case "reply":
|
||||||
|
name, _ := registry.TypeName(&entrytypes.Reply{})
|
||||||
|
v2[i] = name
|
||||||
|
case "photo":
|
||||||
|
name, _ := registry.TypeName(&entrytypes.Image{})
|
||||||
|
v2[i] = name
|
||||||
|
case "note":
|
||||||
|
name, _ := registry.TypeName(&entrytypes.Note{})
|
||||||
|
v2[i] = name
|
||||||
|
case "recipe":
|
||||||
|
name, _ := registry.TypeName(&entrytypes.Recipe{})
|
||||||
|
v2[i] = name
|
||||||
|
case "page":
|
||||||
|
name, _ := registry.TypeName(&entrytypes.Page{})
|
||||||
|
v2[i] = name
|
||||||
|
default:
|
||||||
|
v2[i] = v1Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v2
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package infra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sqlAuthor struct {
|
||||||
|
Name string `db:"name"`
|
||||||
|
PasswordHash string `db:"password_hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultAuthorRepo struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultAuthorRepo(db Database) *DefaultAuthorRepo {
|
||||||
|
sqlxdb := db.Get()
|
||||||
|
|
||||||
|
// Create table if not exists
|
||||||
|
sqlxdb.MustExec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS authors (
|
||||||
|
name TEXT PRIMARY KEY,
|
||||||
|
password_hash TEXT NOT NULL
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
|
return &DefaultAuthorRepo{
|
||||||
|
db: sqlxdb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByName implements repository.AuthorRepository.
|
||||||
|
func (r *DefaultAuthorRepo) FindByName(name string) (*model.Author, error) {
|
||||||
|
var author sqlAuthor
|
||||||
|
err := r.db.Get(&author, "SELECT * FROM authors WHERE name = ?", name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.Author{
|
||||||
|
Name: author.Name,
|
||||||
|
PasswordHash: author.PasswordHash,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create implements repository.AuthorRepository.
|
||||||
|
func (r *DefaultAuthorRepo) Create(name string, passwordHash string) (*model.Author, error) {
|
||||||
|
author := sqlAuthor{
|
||||||
|
Name: name,
|
||||||
|
PasswordHash: passwordHash,
|
||||||
|
}
|
||||||
|
_, err := r.db.NamedExec("INSERT INTO authors (name, password_hash) VALUES (:name, :password_hash)", author)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.Author{
|
||||||
|
Name: author.Name,
|
||||||
|
PasswordHash: author.PasswordHash,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package infra_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/infra"
|
||||||
|
"owl-blogs/test"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupAutherRepo() repository.AuthorRepository {
|
||||||
|
db := test.NewMockDb()
|
||||||
|
repo := infra.NewDefaultAuthorRepo(db)
|
||||||
|
return repo
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorRepoCreate(t *testing.T) {
|
||||||
|
repo := setupAutherRepo()
|
||||||
|
|
||||||
|
author, err := repo.Create("name", "password")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
author, err = repo.FindByName(author.Name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, author.Name, "name")
|
||||||
|
require.Equal(t, author.PasswordHash, "password")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorRepoNoSideEffect(t *testing.T) {
|
||||||
|
repo := setupAutherRepo()
|
||||||
|
|
||||||
|
author, err := repo.Create("name1", "password1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
author2, err := repo.Create("name2", "password2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
author, err = repo.FindByName(author.Name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
author2, err = repo.FindByName(author2.Name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, author.Name, "name1")
|
||||||
|
require.Equal(t, author.PasswordHash, "password1")
|
||||||
|
require.Equal(t, author2.Name, "name2")
|
||||||
|
require.Equal(t, author2.PasswordHash, "password2")
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
package infra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sqlBinaryFile struct {
|
||||||
|
Id string `db:"id"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
EntryId *string `db:"entry_id"`
|
||||||
|
Data []byte `db:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultBinaryFileRepo struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBinaryFileRepo creates a new binary file repository
|
||||||
|
// It creates the table if not exists
|
||||||
|
func NewBinaryFileRepo(db Database) repository.BinaryRepository {
|
||||||
|
sqlxdb := db.Get()
|
||||||
|
|
||||||
|
// Create table if not exists
|
||||||
|
sqlxdb.MustExec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS binary_files (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
entry_id VARCHAR(255),
|
||||||
|
data BLOB NOT NULL
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
|
return &DefaultBinaryFileRepo{db: sqlxdb}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create implements repository.BinaryRepository
|
||||||
|
func (repo *DefaultBinaryFileRepo) Create(name string, data []byte, entry model.Entry) (*model.BinaryFile, error) {
|
||||||
|
parts := strings.Split(name, ".")
|
||||||
|
fileName := strings.Join(parts[:len(parts)-1], ".")
|
||||||
|
fileExt := parts[len(parts)-1]
|
||||||
|
id := fileName + "." + fileExt
|
||||||
|
|
||||||
|
// check if id exists
|
||||||
|
var count int
|
||||||
|
err := repo.db.Get(&count, "SELECT COUNT(*) FROM binary_files WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
counter := 1
|
||||||
|
for {
|
||||||
|
id = fmt.Sprintf("%s-%d.%s", fileName, counter, fileExt)
|
||||||
|
err := repo.db.Get(&count, "SELECT COUNT(*) FROM binary_files WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var entryId *string
|
||||||
|
if entry != nil {
|
||||||
|
eId := entry.ID()
|
||||||
|
entryId = &eId
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = repo.db.Exec("INSERT INTO binary_files (id, name, entry_id, data) VALUES (?, ?, ?, ?)", id, name, entryId, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.BinaryFile{Id: id, Name: name, Data: data}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindById implements repository.BinaryRepository
|
||||||
|
func (repo *DefaultBinaryFileRepo) FindById(id string) (*model.BinaryFile, error) {
|
||||||
|
var sqlFile sqlBinaryFile
|
||||||
|
err := repo.db.Get(&sqlFile, "SELECT * FROM binary_files WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.BinaryFile{Id: sqlFile.Id, Name: sqlFile.Name, Data: sqlFile.Data}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByNameForEntry implements repository.BinaryRepository
|
||||||
|
func (repo *DefaultBinaryFileRepo) FindByNameForEntry(name string, entry model.Entry) (*model.BinaryFile, error) {
|
||||||
|
var sqlFile sqlBinaryFile
|
||||||
|
err := repo.db.Get(&sqlFile, "SELECT * FROM binary_files WHERE name = ? AND entry_id = ?", name, entry.ID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.BinaryFile{Id: sqlFile.Id, Name: sqlFile.Name, Data: sqlFile.Data}, nil
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package infra_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/infra"
|
||||||
|
"owl-blogs/test"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupBinaryRepo() repository.BinaryRepository {
|
||||||
|
db := test.NewMockDb()
|
||||||
|
repo := infra.NewBinaryFileRepo(db)
|
||||||
|
return repo
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBinaryRepoCreate(t *testing.T) {
|
||||||
|
repo := setupBinaryRepo()
|
||||||
|
|
||||||
|
file, err := repo.Create("name", []byte("😀 😃 😄 😁"), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
file, err = repo.FindById(file.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, file.Name, "name")
|
||||||
|
require.Equal(t, file.Data, []byte("😀 😃 😄 😁"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBinaryRepoNoSideEffect(t *testing.T) {
|
||||||
|
repo := setupBinaryRepo()
|
||||||
|
|
||||||
|
file, err := repo.Create("name1", []byte("111"), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
file2, err := repo.Create("name2", []byte("222"), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
file, err = repo.FindById(file.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
file2, err = repo.FindById(file2.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, file.Name, "name1")
|
||||||
|
require.Equal(t, file.Data, []byte("111"))
|
||||||
|
require.Equal(t, file2.Name, "name2")
|
||||||
|
require.Equal(t, file2.Data, []byte("222"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBinaryWithSpaceInName(t *testing.T) {
|
||||||
|
repo := setupBinaryRepo()
|
||||||
|
|
||||||
|
file, err := repo.Create("name with space", []byte("111"), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
file, err = repo.FindById(file.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, file.Name, "name with space")
|
||||||
|
require.Equal(t, file.Data, []byte("111"))
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package infra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DefaultConfigRepo struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigRepo(db Database) repository.ConfigRepository {
|
||||||
|
sqlxdb := db.Get()
|
||||||
|
|
||||||
|
sqlxdb.MustExec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS site_config (
|
||||||
|
name TEXT PRIMARY KEY,
|
||||||
|
config TEXT
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
|
return &DefaultConfigRepo{
|
||||||
|
db: sqlxdb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implements repository.SiteConfigRepository.
|
||||||
|
func (r *DefaultConfigRepo) Get(name string, result interface{}) error {
|
||||||
|
data := []byte{}
|
||||||
|
err := r.db.Get(&data, "SELECT config FROM site_config WHERE name = ?", name)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == "sql: no rows in result set" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update implements repository.SiteConfigRepository.
|
||||||
|
func (r *DefaultConfigRepo) Update(name string, siteConfig interface{}) error {
|
||||||
|
jsonData, err := json.Marshal(siteConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
res, err := r.db.Exec("UPDATE site_config SET config = ? WHERE name = ?", jsonData, name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rows, err := res.RowsAffected()
|
||||||
|
if rows == 0 {
|
||||||
|
_, err = r.db.Exec("INSERT INTO site_config (name, config) VALUES (?, ?)", name, jsonData)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package infra_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"owl-blogs/infra"
|
||||||
|
"owl-blogs/test"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupSiteConfigRepo() repository.ConfigRepository {
|
||||||
|
db := test.NewMockDb()
|
||||||
|
repo := infra.NewConfigRepo(db)
|
||||||
|
return repo
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSiteConfigRepo(t *testing.T) {
|
||||||
|
repo := setupSiteConfigRepo()
|
||||||
|
|
||||||
|
config := model.SiteConfig{}
|
||||||
|
err := repo.Get("test", &config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "", config.Title)
|
||||||
|
require.Equal(t, "", config.SubTitle)
|
||||||
|
|
||||||
|
config.Title = "title"
|
||||||
|
config.SubTitle = "SubTitle"
|
||||||
|
|
||||||
|
err = repo.Update("test", config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
config2 := model.SiteConfig{}
|
||||||
|
err = repo.Get("test", &config2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "title", config2.Title)
|
||||||
|
require.Equal(t, "SubTitle", config2.SubTitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSiteConfigUpdates(t *testing.T) {
|
||||||
|
repo := setupSiteConfigRepo()
|
||||||
|
config := model.SiteConfig{}
|
||||||
|
err := repo.Get("test", &config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "", config.Title)
|
||||||
|
require.Equal(t, "", config.SubTitle)
|
||||||
|
|
||||||
|
config.Title = "title"
|
||||||
|
config.SubTitle = "SubTitle"
|
||||||
|
|
||||||
|
err = repo.Update("test", config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
config2 := model.SiteConfig{}
|
||||||
|
err = repo.Get("test", &config2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "title", config2.Title)
|
||||||
|
require.Equal(t, "SubTitle", config2.SubTitle)
|
||||||
|
|
||||||
|
config2.Title = "title2"
|
||||||
|
config2.SubTitle = "SubTitle2"
|
||||||
|
|
||||||
|
err = repo.Update("test", config2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
config3 := model.SiteConfig{}
|
||||||
|
err = repo.Get("test", &config3)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "title2", config3.Title)
|
||||||
|
require.Equal(t, "SubTitle2", config3.SubTitle)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
package infra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"owl-blogs/app"
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sqlEntry struct {
|
||||||
|
Id string `db:"id"`
|
||||||
|
Type string `db:"type"`
|
||||||
|
PublishedAt *time.Time `db:"published_at"`
|
||||||
|
MetaData *string `db:"meta_data"`
|
||||||
|
AuthorId string `db:"author_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultEntryRepo struct {
|
||||||
|
typeRegistry *app.EntryTypeRegistry
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create implements repository.EntryRepository.
|
||||||
|
func (r *DefaultEntryRepo) Create(entry model.Entry) error {
|
||||||
|
t, err := r.typeRegistry.TypeName(entry)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("entry type not registered")
|
||||||
|
}
|
||||||
|
|
||||||
|
var metaDataJson []byte
|
||||||
|
if entry.MetaData() != nil {
|
||||||
|
metaDataJson, _ = json.Marshal(entry.MetaData())
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.ID() == "" {
|
||||||
|
entry.SetID(uuid.New().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = r.db.Exec("INSERT INTO entries (id, type, published_at, author_id, meta_data) VALUES (?, ?, ?, ?, ?)", entry.ID(), t, entry.PublishedAt(), entry.AuthorId(), metaDataJson)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete implements repository.EntryRepository.
|
||||||
|
func (r *DefaultEntryRepo) Delete(entry model.Entry) error {
|
||||||
|
_, err := r.db.Exec("DELETE FROM entries WHERE id = ?", entry.ID())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindAll implements repository.EntryRepository.
|
||||||
|
func (r *DefaultEntryRepo) FindAll(types *[]string) ([]model.Entry, error) {
|
||||||
|
filterStr := ""
|
||||||
|
if types != nil {
|
||||||
|
filters := []string{}
|
||||||
|
for _, t := range *types {
|
||||||
|
filters = append(filters, "type = '"+t+"'")
|
||||||
|
}
|
||||||
|
filterStr = strings.Join(filters, " OR ")
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []sqlEntry
|
||||||
|
if filterStr != "" {
|
||||||
|
err := r.db.Select(&entries, "SELECT * FROM entries WHERE "+filterStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := r.db.Select(&entries, "SELECT * FROM entries")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := []model.Entry{}
|
||||||
|
for _, entry := range entries {
|
||||||
|
e, err := r.sqlEntryToEntry(entry)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result = append(result, e)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindById implements repository.EntryRepository.
|
||||||
|
func (r *DefaultEntryRepo) FindById(id string) (model.Entry, error) {
|
||||||
|
data := sqlEntry{}
|
||||||
|
err := r.db.Get(&data, "SELECT * FROM entries WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if data.Id == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return r.sqlEntryToEntry(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update implements repository.EntryRepository.
|
||||||
|
func (r *DefaultEntryRepo) Update(entry model.Entry) error {
|
||||||
|
exEntry, _ := r.FindById(entry.ID())
|
||||||
|
if exEntry == nil {
|
||||||
|
return errors.New("entry not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := r.typeRegistry.TypeName(entry)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("entry type not registered")
|
||||||
|
}
|
||||||
|
|
||||||
|
var metaDataJson []byte
|
||||||
|
if entry.MetaData() != nil {
|
||||||
|
metaDataJson, _ = json.Marshal(entry.MetaData())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = r.db.Exec("UPDATE entries SET published_at = ?, author_id = ?, meta_data = ? WHERE id = ?", entry.PublishedAt(), entry.AuthorId(), metaDataJson, entry.ID())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEntryRepository(db Database, register *app.EntryTypeRegistry) repository.EntryRepository {
|
||||||
|
sqlxdb := db.Get()
|
||||||
|
|
||||||
|
// Create tables if not exists
|
||||||
|
sqlxdb.MustExec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS entries (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
published_at DATETIME,
|
||||||
|
author_id TEXT NOT NULL,
|
||||||
|
meta_data TEXT NOT NULL
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
|
return &DefaultEntryRepo{
|
||||||
|
db: sqlxdb,
|
||||||
|
typeRegistry: register,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DefaultEntryRepo) sqlEntryToEntry(entry sqlEntry) (model.Entry, error) {
|
||||||
|
e, err := r.typeRegistry.Type(entry.Type)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("entry type not registered")
|
||||||
|
}
|
||||||
|
metaData := reflect.New(reflect.TypeOf(e.MetaData()).Elem()).Interface()
|
||||||
|
json.Unmarshal([]byte(*entry.MetaData), metaData)
|
||||||
|
e.SetID(entry.Id)
|
||||||
|
e.SetPublishedAt(entry.PublishedAt)
|
||||||
|
e.SetMetaData(metaData)
|
||||||
|
e.SetAuthorId(entry.AuthorId)
|
||||||
|
return e, nil
|
||||||
|
}
|
|
@ -0,0 +1,192 @@
|
||||||
|
package infra_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/app"
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/infra"
|
||||||
|
"owl-blogs/test"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupRepo() repository.EntryRepository {
|
||||||
|
db := test.NewMockDb()
|
||||||
|
register := app.NewEntryTypeRegistry()
|
||||||
|
register.Register(&test.MockEntry{})
|
||||||
|
repo := infra.NewEntryRepository(db, register)
|
||||||
|
return repo
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepoCreate(t *testing.T) {
|
||||||
|
repo := setupRepo()
|
||||||
|
|
||||||
|
entry := &test.MockEntry{}
|
||||||
|
now := time.Now()
|
||||||
|
entry.SetPublishedAt(&now)
|
||||||
|
entry.SetAuthorId("authorId")
|
||||||
|
entry.SetMetaData(&test.MockEntryMetaData{
|
||||||
|
Str: "str",
|
||||||
|
Number: 1,
|
||||||
|
Date: now,
|
||||||
|
})
|
||||||
|
err := repo.Create(entry)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
entry2, err := repo.FindById(entry.ID())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, entry.ID(), entry2.ID())
|
||||||
|
require.Equal(t, entry.Content(), entry2.Content())
|
||||||
|
require.Equal(t, entry.AuthorId(), entry2.AuthorId())
|
||||||
|
require.Equal(t, entry.PublishedAt().Unix(), entry2.PublishedAt().Unix())
|
||||||
|
meta := entry.MetaData().(*test.MockEntryMetaData)
|
||||||
|
meta2 := entry2.MetaData().(*test.MockEntryMetaData)
|
||||||
|
require.Equal(t, meta.Str, meta2.Str)
|
||||||
|
require.Equal(t, meta.Number, meta2.Number)
|
||||||
|
require.Equal(t, meta.Date.Unix(), meta2.Date.Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepoDelete(t *testing.T) {
|
||||||
|
repo := setupRepo()
|
||||||
|
|
||||||
|
entry := &test.MockEntry{}
|
||||||
|
now := time.Now()
|
||||||
|
entry.SetPublishedAt(&now)
|
||||||
|
entry.SetMetaData(&test.MockEntryMetaData{
|
||||||
|
Str: "str",
|
||||||
|
Number: 1,
|
||||||
|
Date: now,
|
||||||
|
})
|
||||||
|
err := repo.Create(entry)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = repo.Delete(entry)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = repo.FindById("id")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepoFindAll(t *testing.T) {
|
||||||
|
repo := setupRepo()
|
||||||
|
|
||||||
|
entry := &test.MockEntry{}
|
||||||
|
now := time.Now()
|
||||||
|
entry.SetPublishedAt(&now)
|
||||||
|
entry.SetMetaData(&test.MockEntryMetaData{
|
||||||
|
Str: "str",
|
||||||
|
Number: 1,
|
||||||
|
Date: now,
|
||||||
|
})
|
||||||
|
err := repo.Create(entry)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
entry2 := &test.MockEntry{}
|
||||||
|
now2 := time.Now()
|
||||||
|
entry2.SetPublishedAt(&now2)
|
||||||
|
entry2.SetMetaData(&test.MockEntryMetaData{
|
||||||
|
Str: "str",
|
||||||
|
Number: 1,
|
||||||
|
Date: now,
|
||||||
|
})
|
||||||
|
|
||||||
|
err = repo.Create(entry2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
entries, err := repo.FindAll(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(entries))
|
||||||
|
|
||||||
|
entries, err = repo.FindAll(&[]string{"MockEntry"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(entries))
|
||||||
|
|
||||||
|
entries, err = repo.FindAll(&[]string{"MockEntry2"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 0, len(entries))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepoUpdate(t *testing.T) {
|
||||||
|
repo := setupRepo()
|
||||||
|
|
||||||
|
entry := &test.MockEntry{}
|
||||||
|
now := time.Now()
|
||||||
|
entry.SetPublishedAt(&now)
|
||||||
|
entry.SetAuthorId("authorId")
|
||||||
|
entry.SetMetaData(&test.MockEntryMetaData{
|
||||||
|
Str: "str",
|
||||||
|
Number: 1,
|
||||||
|
Date: now,
|
||||||
|
})
|
||||||
|
err := repo.Create(entry)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
entry2 := &test.MockEntry{}
|
||||||
|
now2 := time.Now()
|
||||||
|
entry2.SetPublishedAt(&now2)
|
||||||
|
entry.SetAuthorId("authorId2")
|
||||||
|
entry2.SetMetaData(&test.MockEntryMetaData{
|
||||||
|
Str: "str2",
|
||||||
|
Number: 2,
|
||||||
|
Date: now2,
|
||||||
|
})
|
||||||
|
err = repo.Create(entry2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = repo.Update(entry2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
entry3, err := repo.FindById(entry2.ID())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, entry3.Content(), entry2.Content())
|
||||||
|
require.Equal(t, entry3.PublishedAt().Unix(), entry2.PublishedAt().Unix())
|
||||||
|
require.Equal(t, entry3.AuthorId(), entry2.AuthorId())
|
||||||
|
meta := entry3.MetaData().(*test.MockEntryMetaData)
|
||||||
|
meta2 := entry2.MetaData().(*test.MockEntryMetaData)
|
||||||
|
require.Equal(t, meta.Str, meta2.Str)
|
||||||
|
require.Equal(t, meta.Number, meta2.Number)
|
||||||
|
require.Equal(t, meta.Date.Unix(), meta2.Date.Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepoNoSideEffect(t *testing.T) {
|
||||||
|
repo := setupRepo()
|
||||||
|
|
||||||
|
entry1 := &test.MockEntry{}
|
||||||
|
now1 := time.Now()
|
||||||
|
entry1.SetPublishedAt(&now1)
|
||||||
|
entry1.SetMetaData(&test.MockEntryMetaData{
|
||||||
|
Str: "1",
|
||||||
|
Number: 1,
|
||||||
|
Date: now1,
|
||||||
|
})
|
||||||
|
|
||||||
|
err := repo.Create(entry1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
entry2 := &test.MockEntry{}
|
||||||
|
now2 := time.Now()
|
||||||
|
entry2.SetPublishedAt(&now2)
|
||||||
|
entry2.SetMetaData(&test.MockEntryMetaData{
|
||||||
|
Str: "2",
|
||||||
|
Number: 2,
|
||||||
|
Date: now2,
|
||||||
|
})
|
||||||
|
err = repo.Create(entry2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
r1, err := repo.FindById(entry1.ID())
|
||||||
|
require.NoError(t, err)
|
||||||
|
r2, err := repo.FindById(entry2.ID())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, r1.MetaData().(*test.MockEntryMetaData).Str, "1")
|
||||||
|
require.Equal(t, r1.MetaData().(*test.MockEntryMetaData).Number, 1)
|
||||||
|
require.Equal(t, r1.MetaData().(*test.MockEntryMetaData).Date.Unix(), now1.Unix())
|
||||||
|
|
||||||
|
require.Equal(t, r2.MetaData().(*test.MockEntryMetaData).Str, "2")
|
||||||
|
require.Equal(t, r2.MetaData().(*test.MockEntryMetaData).Number, 2)
|
||||||
|
require.Equal(t, r2.MetaData().(*test.MockEntryMetaData).Date.Unix(), now2.Unix())
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package infra
|
||||||
|
|
||||||
|
import "github.com/jmoiron/sqlx"
|
||||||
|
|
||||||
|
type Database interface {
|
||||||
|
Get() *sqlx.DB
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package infra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SqliteDatabase struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSqliteDB(path string) Database {
|
||||||
|
db := sqlx.MustOpen("sqlite3", path)
|
||||||
|
return &SqliteDatabase{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SqliteDatabase) Get() *sqlx.DB {
|
||||||
|
return d.db
|
||||||
|
}
|
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))
|
|
||||||
// }
|
|
||||||
// }
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"embed"
|
||||||
|
"io"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
"github.com/yuin/goldmark/extension"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
"github.com/yuin/goldmark/renderer/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TemplateData struct {
|
||||||
|
Data interface{}
|
||||||
|
SiteConfig model.SiteConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed templates
|
||||||
|
var templates embed.FS
|
||||||
|
|
||||||
|
var funcMap = template.FuncMap{
|
||||||
|
"markdown": func(text string) string {
|
||||||
|
html, err := RenderMarkdown(text)
|
||||||
|
if err != nil {
|
||||||
|
return ">>>could not render markdown<<<"
|
||||||
|
}
|
||||||
|
return html
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateTemplateWithBase(templateName string) (*template.Template, error) {
|
||||||
|
|
||||||
|
return template.New(templateName).Funcs(funcMap).ParseFS(
|
||||||
|
templates,
|
||||||
|
"templates/base.tmpl",
|
||||||
|
"templates/"+templateName+".tmpl",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderTemplateWithBase(w io.Writer, siteConfig model.SiteConfig, templateName string, data interface{}) error {
|
||||||
|
|
||||||
|
t, err := CreateTemplateWithBase(templateName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = t.ExecuteTemplate(w, "base", TemplateData{
|
||||||
|
Data: data,
|
||||||
|
SiteConfig: siteConfig,
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderTemplateToString(templateName string, data interface{}) (string, error) {
|
||||||
|
tmplStr, _ := templates.ReadFile("templates/" + templateName + ".tmpl")
|
||||||
|
|
||||||
|
t, err := template.New("templates/" + templateName + ".tmpl").Funcs(funcMap).Parse(string(tmplStr))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var output bytes.Buffer
|
||||||
|
|
||||||
|
err = t.Execute(&output, data)
|
||||||
|
return output.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderMarkdown(mdText string) (string, error) {
|
||||||
|
markdown := goldmark.New(
|
||||||
|
goldmark.WithRendererOptions(
|
||||||
|
html.WithUnsafe(),
|
||||||
|
),
|
||||||
|
goldmark.WithExtensions(
|
||||||
|
// meta.Meta,
|
||||||
|
extension.GFM,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
context := parser.NewContext()
|
||||||
|
err := markdown.Convert([]byte(mdText), &buf, parser.WithContext(context))
|
||||||
|
|
||||||
|
return buf.String(), err
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
{{define "base"}}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang='en'>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>{{template "title" .Data}} - {{ .SiteConfig.Title }}</title>
|
||||||
|
<meta property="og:title" content="{{template "title" .Data}}" />
|
||||||
|
|
||||||
|
<link rel='stylesheet' href='/static/pico.min.css'>
|
||||||
|
<style>
|
||||||
|
header {
|
||||||
|
background-color: {{.SiteConfig.HeaderColor}};
|
||||||
|
padding-bottom: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: dashed 2px;
|
||||||
|
border-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
float: left;
|
||||||
|
margin-right: 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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="/">{{ .SiteConfig.Title }}</a></h2>
|
||||||
|
<h3 class="p-note">{{ .SiteConfig.SubTitle }}</h3>
|
||||||
|
</hgroup>
|
||||||
|
|
||||||
|
<div class="header-profile">
|
||||||
|
{{ if .SiteConfig.AvatarUrl }}
|
||||||
|
<img class="u-photo u-logo avatar" src="{{ .SiteConfig.AvatarUrl }}" alt="{{ .SiteConfig.Title }}" width="100" height="100" />
|
||||||
|
{{ end }}
|
||||||
|
<div style="float: right; list-style: none;">
|
||||||
|
{{ range $me := .SiteConfig.Me }}
|
||||||
|
<li><a href="{{$me.Url}}" rel="me">{{$me.Name}}</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
{{ range $link := .SiteConfig.HeaderMenu }}
|
||||||
|
{{ if $link.List }}
|
||||||
|
<li><a href="/lists/{{ $link.List }}">{{ $link.Title }}</a></li>
|
||||||
|
{{ else if $link.Post }}
|
||||||
|
<li><a href="/posts/{{ $link.Post }}">{{ $link.Title }}</a></li>
|
||||||
|
{{ else }}
|
||||||
|
<li><a href="{{ $link.Url }}">{{ $link.Title }}</a></li>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="container">
|
||||||
|
{{template "main" .Data}}
|
||||||
|
</main>
|
||||||
|
<footer class="container">
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
{{ range $link := .SiteConfig.FooterMenu }}
|
||||||
|
{{ if $link.List }}
|
||||||
|
<li><a href="/lists/{{ $link.List }}">{{ $link.Title }}</a></li>
|
||||||
|
{{ else if $link.Post }}
|
||||||
|
<li><a href="/posts/{{ $link.Post }}">{{ $link.Title }}</a></li>
|
||||||
|
{{ else }}
|
||||||
|
<li><a href="{{ $link.Url }}">{{ $link.Title }}</a></li>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -0,0 +1 @@
|
||||||
|
{{.MetaData.Content | markdown }}
|
|
@ -0,0 +1,3 @@
|
||||||
|
Bookmark: <a href="{{.MetaData.Url}}">{{.MetaData.Url}}</a>
|
||||||
|
|
||||||
|
{{.MetaData.Content | markdown }}
|
|
@ -0,0 +1,4 @@
|
||||||
|
<img src="/media/{{.MetaData.ImageId}}">
|
||||||
|
|
||||||
|
{{.MetaData.Content}}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{{.MetaData.Content | markdown }}
|
|
@ -0,0 +1 @@
|
||||||
|
{{.MetaData.Content | markdown }}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue