Index: VERSION ================================================================== --- VERSION +++ VERSION @@ -1,1 +1,1 @@ -0.0.12 +0.0.13 Index: ast/ast.go ================================================================== --- ast/ast.go +++ ast/ast.go @@ -81,14 +81,14 @@ // RefState indicates the state of the reference. type RefState int // Constants for RefState const ( - RefStateInvalid RefState = iota // Invalid Referende + RefStateInvalid RefState = iota // Invalid Reference RefStateZettel // Reference to an internal zettel RefStateSelf // Reference to same zettel with a fragment RefStateFound // Reference to an existing internal zettel RefStateBroken // Reference to a non-existing internal zettel RefStateHosted // Reference to local hosted non-Zettel, without URL change RefStateBased // Reference to local non-Zettel, to be prefixed RefStateExternal // Reference to external material ) Index: ast/ref.go ================================================================== --- ast/ref.go +++ ast/ref.go @@ -17,11 +17,12 @@ "zettelstore.de/z/domain/id" ) // ParseReference parses a string and returns a reference. func ParseReference(s string) *Reference { - if s == "" { + switch s { + case "", "00000000000000": return &Reference{URL: nil, Value: s, State: RefStateInvalid} } if state, ok := localState(s); ok { if state == RefStateBased { s = s[1:] Index: ast/ref_test.go ================================================================== --- ast/ref_test.go +++ ast/ref_test.go @@ -46,10 +46,11 @@ isZettel bool isExternal bool isLocal bool }{ {"", false, false, false}, + {"00000000000000", false, false, false}, {"http://zettelstore.de/z/ast", false, true, false}, {"12345678901234", true, false, false}, {"12345678901234#local", true, false, false}, {"http://12345678901234", false, true, false}, {"http://zettelstore.de/z/12345678901234", false, true, false}, ADDED auth/auth.go Index: auth/auth.go ================================================================== --- auth/auth.go +++ auth/auth.go @@ -0,0 +1,101 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package auth provides services for authentification / authorization. +package auth + +import ( + "time" + + "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/place" + "zettelstore.de/z/web/server" +) + +// BaseManager allows to check some base auth modes. +type BaseManager interface { + // IsReadonly returns true, if the systems is configured to run in read-only-mode. + IsReadonly() bool +} + +// TokenManager provides methods to create authentication +type TokenManager interface { + + // GetToken produces a authentication token. + GetToken(ident *meta.Meta, d time.Duration, kind TokenKind) ([]byte, error) + + // CheckToken checks the validity of the token and returns relevant data. + CheckToken(token []byte, k TokenKind) (TokenData, error) +} + +// TokenKind specifies for which application / usage a token is/was requested. +type TokenKind int + +// Allowed values of token kind +const ( + _ TokenKind = iota + KindJSON + KindHTML +) + +// TokenData contains some important elements from a token. +type TokenData struct { + Token []byte + Now time.Time + Issued time.Time + Expires time.Time + Ident string + Zid id.Zid +} + +// AuthzManager provides methods for authorization. +type AuthzManager interface { + BaseManager + + // Owner returns the zettel identifier of the owner. + Owner() id.Zid + + // IsOwner returns true, if the given zettel identifier is that of the owner. + IsOwner(zid id.Zid) bool + + // Returns true if authentication is enabled. + WithAuth() bool + + // GetUserRole role returns the user role of the given user zettel. + GetUserRole(user *meta.Meta) meta.UserRole +} + +// Manager is the main interface for providing the service. +type Manager interface { + TokenManager + AuthzManager + + PlaceWithPolicy(auth server.Auth, unprotectedPlace place.Place, rtConfig config.Config) (place.Place, Policy) +} + +// Policy is an interface for checking access authorization. +type Policy interface { + // User is allowed to create a new zettel. + CanCreate(user, newMeta *meta.Meta) bool + + // User is allowed to read zettel + CanRead(user, m *meta.Meta) bool + + // User is allowed to write zettel. + CanWrite(user, oldMeta, newMeta *meta.Meta) bool + + // User is allowed to rename zettel + CanRename(user, m *meta.Meta) bool + + // User is allowed to delete zettel + CanDelete(user, m *meta.Meta) bool +} ADDED auth/impl/impl.go Index: auth/impl/impl.go ================================================================== --- auth/impl/impl.go +++ auth/impl/impl.go @@ -0,0 +1,184 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package impl provides services for authentification / authorization. +package impl + +import ( + "errors" + "hash/fnv" + "io" + "time" + + "github.com/pascaldekloe/jwt" + + "zettelstore.de/z/auth" + "zettelstore.de/z/auth/policy" + "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/kernel" + "zettelstore.de/z/place" + "zettelstore.de/z/web/server" +) + +type myAuth struct { + readonly bool + owner id.Zid + secret []byte +} + +// New creates a new auth object. +func New(readonly bool, owner id.Zid, extSecret string) auth.Manager { + return &myAuth{ + readonly: readonly, + owner: owner, + secret: calcSecret(extSecret), + } +} + +var configKeys = []string{ + kernel.CoreProgname, + kernel.CoreGoVersion, + kernel.CoreHostname, + kernel.CoreGoOS, + kernel.CoreGoArch, + kernel.CoreVersion, +} + +func calcSecret(extSecret string) []byte { + h := fnv.New128() + if extSecret != "" { + io.WriteString(h, extSecret) + } + for _, key := range configKeys { + io.WriteString(h, kernel.Main.GetConfig(kernel.CoreService, key).(string)) + } + return h.Sum(nil) +} + +// IsReadonly returns true, if the systems is configured to run in read-only-mode. +func (a *myAuth) IsReadonly() bool { return a.readonly } + +const reqHash = jwt.HS512 + +// ErrNoUser signals that the meta data has no role value 'user'. +var ErrNoUser = errors.New("auth: meta is no user") + +// ErrNoIdent signals that the 'ident' key is missing. +var ErrNoIdent = errors.New("auth: missing ident") + +// ErrOtherKind signals that the token was defined for another token kind. +var ErrOtherKind = errors.New("auth: wrong token kind") + +// ErrNoZid signals that the 'zid' key is missing. +var ErrNoZid = errors.New("auth: missing zettel id") + +// GetToken returns a token to be used for authentification. +func (a *myAuth) GetToken(ident *meta.Meta, d time.Duration, kind auth.TokenKind) ([]byte, error) { + if role, ok := ident.Get(meta.KeyRole); !ok || role != meta.ValueRoleUser { + return nil, ErrNoUser + } + subject, ok := ident.Get(meta.KeyUserID) + if !ok || subject == "" { + return nil, ErrNoIdent + } + + now := time.Now().Round(time.Second) + claims := jwt.Claims{ + Registered: jwt.Registered{ + Subject: subject, + Expires: jwt.NewNumericTime(now.Add(d)), + Issued: jwt.NewNumericTime(now), + }, + Set: map[string]interface{}{ + "zid": ident.Zid.String(), + "_tk": int(kind), + }, + } + token, err := claims.HMACSign(reqHash, a.secret) + if err != nil { + return nil, err + } + return token, nil +} + +// ErrTokenExpired signals an exired token +var ErrTokenExpired = errors.New("auth: token expired") + +// CheckToken checks the validity of the token and returns relevant data. +func (a *myAuth) CheckToken(token []byte, k auth.TokenKind) (auth.TokenData, error) { + h, err := jwt.NewHMAC(reqHash, a.secret) + if err != nil { + return auth.TokenData{}, err + } + claims, err := h.Check(token) + if err != nil { + return auth.TokenData{}, err + } + now := time.Now().Round(time.Second) + expires := claims.Expires.Time() + if expires.Before(now) { + return auth.TokenData{}, ErrTokenExpired + } + ident := claims.Subject + if ident == "" { + return auth.TokenData{}, ErrNoIdent + } + if zidS, ok := claims.Set["zid"].(string); ok { + if zid, err := id.Parse(zidS); err == nil { + if kind, ok := claims.Set["_tk"].(float64); ok { + if auth.TokenKind(kind) == k { + return auth.TokenData{ + Token: token, + Now: now, + Issued: claims.Issued.Time(), + Expires: expires, + Ident: ident, + Zid: zid, + }, nil + } + } + return auth.TokenData{}, ErrOtherKind + } + } + return auth.TokenData{}, ErrNoZid +} + +func (a *myAuth) Owner() id.Zid { return a.owner } + +func (a *myAuth) IsOwner(zid id.Zid) bool { + return zid.IsValid() && zid == a.owner +} + +func (a *myAuth) WithAuth() bool { return a.owner != id.Invalid } + +// GetUserRole role returns the user role of the given user zettel. +func (a *myAuth) GetUserRole(user *meta.Meta) meta.UserRole { + if user == nil { + if a.WithAuth() { + return meta.UserRoleUnknown + } + return meta.UserRoleOwner + } + if a.IsOwner(user.Zid) { + return meta.UserRoleOwner + } + if val, ok := user.Get(meta.KeyUserRole); ok { + if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { + return ur + } + } + return meta.UserRoleReader +} + +func (a *myAuth) PlaceWithPolicy(auth server.Auth, unprotectedPlace place.Place, rtConfig config.Config) (place.Place, auth.Policy) { + return policy.PlaceWithPolicy(auth, a, unprotectedPlace, rtConfig) +} Index: auth/policy/anon.go ================================================================== --- auth/policy/anon.go +++ auth/policy/anon.go @@ -10,18 +10,18 @@ // Package policy provides some interfaces and implementation for authorization policies. package policy import ( + "zettelstore.de/z/auth" + "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" ) type anonPolicy struct { - simpleMode bool - expertMode func() bool - getVisibility func(*meta.Meta) meta.Visibility - pre Policy + authConfig config.AuthConfig + pre auth.Policy } func (ap *anonPolicy) CanCreate(user, newMeta *meta.Meta) bool { return ap.pre.CanCreate(user, newMeta) } @@ -41,13 +41,10 @@ func (ap *anonPolicy) CanDelete(user, m *meta.Meta) bool { return ap.pre.CanDelete(user, m) && ap.checkVisibility(m) } func (ap *anonPolicy) checkVisibility(m *meta.Meta) bool { - switch ap.getVisibility(m) { - case meta.VisibilitySimple: - return ap.simpleMode || ap.expertMode() - case meta.VisibilityExpert: - return ap.expertMode() + if ap.authConfig.GetVisibility(m) == meta.VisibilityExpert { + return ap.authConfig.GetExpertMode() } return true } Index: auth/policy/default.go ================================================================== --- auth/policy/default.go +++ auth/policy/default.go @@ -10,15 +10,17 @@ // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( - "zettelstore.de/z/config/runtime" + "zettelstore.de/z/auth" "zettelstore.de/z/domain/meta" ) -type defaultPolicy struct{} +type defaultPolicy struct { + manager auth.AuthzManager +} func (d *defaultPolicy) CanCreate(user, newMeta *meta.Meta) bool { return true } func (d *defaultPolicy) CanRead(user, m *meta.Meta) bool { return true } func (d *defaultPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return d.canChange(user, oldMeta) @@ -38,11 +40,11 @@ // No authentication: check for owner-like restriction, because the user // acts as an owner return metaRo != meta.ValueUserRoleOwner && !meta.BoolValue(metaRo) } - userRole := runtime.GetUserRole(user) + userRole := d.manager.GetUserRole(user) switch metaRo { case meta.ValueUserRoleReader: return userRole > meta.UserRoleReader case meta.ValueUserRoleWriter: return userRole > meta.UserRoleWriter Index: auth/policy/owner.go ================================================================== --- auth/policy/owner.go +++ auth/policy/owner.go @@ -10,20 +10,19 @@ // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( - "zettelstore.de/z/config/runtime" - "zettelstore.de/z/domain/id" + "zettelstore.de/z/auth" + "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" ) type ownerPolicy struct { - expertMode func() bool - isOwner func(id.Zid) bool - getVisibility func(*meta.Meta) meta.Visibility - pre Policy + manager auth.AuthzManager + authConfig config.AuthConfig + pre auth.Policy } func (o *ownerPolicy) CanCreate(user, newMeta *meta.Meta) bool { if user == nil || !o.pre.CanCreate(user, newMeta) { return false @@ -30,11 +29,11 @@ } return o.userIsOwner(user) || o.userCanCreate(user, newMeta) } func (o *ownerPolicy) userCanCreate(user, newMeta *meta.Meta) bool { - if runtime.GetUserRole(user) == meta.UserRoleReader { + if o.manager.GetUserRole(user) == meta.UserRoleReader { return false } if role, ok := newMeta.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { return false } @@ -42,20 +41,20 @@ } func (o *ownerPolicy) CanRead(user, m *meta.Meta) bool { // No need to call o.pre.CanRead(user, meta), because it will always return true. // Both the default and the readonly policy allow to read a zettel. - vis := o.getVisibility(m) + vis := o.authConfig.GetVisibility(m) if res, ok := o.checkVisibility(user, vis); ok { return res } return o.userIsOwner(user) || o.userCanRead(user, m, vis) } func (o *ownerPolicy) userCanRead(user, m *meta.Meta, vis meta.Visibility) bool { switch vis { - case meta.VisibilityOwner, meta.VisibilitySimple, meta.VisibilityExpert: + case meta.VisibilityOwner, meta.VisibilityExpert: return false case meta.VisibilityPublic: return true } if user == nil { @@ -77,11 +76,11 @@ func (o *ownerPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { if user == nil || !o.pre.CanWrite(user, oldMeta, newMeta) { return false } - vis := o.getVisibility(oldMeta) + vis := o.authConfig.GetVisibility(oldMeta) if res, ok := o.checkVisibility(user, vis); ok { return res } if o.userIsOwner(user) { return true @@ -97,51 +96,50 @@ return false } } return true } - if runtime.GetUserRole(user) == meta.UserRoleReader { + if o.manager.GetUserRole(user) == meta.UserRoleReader { return false } return o.userCanCreate(user, newMeta) } func (o *ownerPolicy) CanRename(user, m *meta.Meta) bool { if user == nil || !o.pre.CanRename(user, m) { return false } - if res, ok := o.checkVisibility(user, o.getVisibility(m)); ok { + if res, ok := o.checkVisibility(user, o.authConfig.GetVisibility(m)); ok { return res } return o.userIsOwner(user) } func (o *ownerPolicy) CanDelete(user, m *meta.Meta) bool { if user == nil || !o.pre.CanDelete(user, m) { return false } - if res, ok := o.checkVisibility(user, o.getVisibility(m)); ok { + if res, ok := o.checkVisibility(user, o.authConfig.GetVisibility(m)); ok { return res } return o.userIsOwner(user) } func (o *ownerPolicy) checkVisibility(user *meta.Meta, vis meta.Visibility) (bool, bool) { - switch vis { - case meta.VisibilitySimple, meta.VisibilityExpert: - return o.userIsOwner(user) && o.expertMode(), true + if vis == meta.VisibilityExpert { + return o.userIsOwner(user) && o.authConfig.GetExpertMode(), true } return false, false } func (o *ownerPolicy) userIsOwner(user *meta.Meta) bool { if user == nil { return false } - if o.isOwner(user.Zid) { + if o.manager.IsOwner(user.Zid) { return true } if val, ok := user.Get(meta.KeyUserRole); ok && val == meta.ValueUserRoleOwner { return true } return false } Index: auth/policy/place.go ================================================================== --- auth/policy/place.go +++ auth/policy/place.go @@ -11,42 +11,44 @@ // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "context" + "io" + "zettelstore.de/z/auth" + "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/search" - "zettelstore.de/z/web/session" + "zettelstore.de/z/web/server" ) // PlaceWithPolicy wraps the given place inside a policy place. func PlaceWithPolicy( + auth server.Auth, + manager auth.AuthzManager, place place.Place, - simpleMode bool, - withAuth func() bool, - isReadOnlyMode bool, - expertMode func() bool, - isOwner func(id.Zid) bool, - getVisibility func(*meta.Meta) meta.Visibility, -) (place.Place, Policy) { - pol := newPolicy(simpleMode, withAuth, isReadOnlyMode, expertMode, isOwner, getVisibility) - return newPlace(place, pol), pol + authConfig config.AuthConfig, +) (place.Place, auth.Policy) { + pol := newPolicy(manager, authConfig) + return newPlace(auth, place, pol), pol } // polPlace implements a policy place. type polPlace struct { + auth server.Auth place place.Place - policy Policy + policy auth.Policy } // newPlace creates a new policy place. -func newPlace(place place.Place, policy Policy) place.Place { +func newPlace(auth server.Auth, place place.Place, policy auth.Policy) place.Place { return &polPlace{ + auth: auth, place: place, policy: policy, } } @@ -57,11 +59,11 @@ func (pp *polPlace) CanCreateZettel(ctx context.Context) bool { return pp.place.CanCreateZettel(ctx) } func (pp *polPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { - user := session.GetUser(ctx) + user := pp.auth.GetUser(ctx) if pp.policy.CanCreate(user, zettel.Meta) { return pp.place.CreateZettel(ctx, zettel) } return id.Invalid, place.NewErrNotAllowed("Create", user, id.Invalid) } @@ -69,11 +71,11 @@ func (pp *polPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { zettel, err := pp.place.GetZettel(ctx, zid) if err != nil { return domain.Zettel{}, err } - user := session.GetUser(ctx) + user := pp.auth.GetUser(ctx) if pp.policy.CanRead(user, zettel.Meta) { return zettel, nil } return domain.Zettel{}, place.NewErrNotAllowed("GetZettel", user, zid) } @@ -81,23 +83,23 @@ func (pp *polPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := pp.place.GetMeta(ctx, zid) if err != nil { return nil, err } - user := session.GetUser(ctx) + user := pp.auth.GetUser(ctx) if pp.policy.CanRead(user, m) { return m, nil } return nil, place.NewErrNotAllowed("GetMeta", user, zid) } func (pp *polPlace) FetchZids(ctx context.Context) (id.Set, error) { - return nil, place.NewErrNotAllowed("fetch-zids", session.GetUser(ctx), id.Invalid) + return nil, place.NewErrNotAllowed("fetch-zids", pp.auth.GetUser(ctx), id.Invalid) } func (pp *polPlace) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { - user := session.GetUser(ctx) + user := pp.auth.GetUser(ctx) canRead := pp.policy.CanRead s = s.AddPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) return pp.place.SelectMeta(ctx, s) } @@ -105,11 +107,11 @@ return pp.place.CanUpdateZettel(ctx, zettel) } func (pp *polPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { zid := zettel.Meta.Zid - user := session.GetUser(ctx) + user := pp.auth.GetUser(ctx) if !zid.IsValid() { return &place.ErrInvalidID{Zid: zid} } // Write existing zettel oldMeta, err := pp.place.GetMeta(ctx, zid) @@ -129,11 +131,11 @@ func (pp *polPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { meta, err := pp.place.GetMeta(ctx, curZid) if err != nil { return err } - user := session.GetUser(ctx) + user := pp.auth.GetUser(ctx) if pp.policy.CanRename(user, meta) { return pp.place.RenameZettel(ctx, curZid, newZid) } return place.NewErrNotAllowed("Rename", user, curZid) } @@ -145,15 +147,19 @@ func (pp *polPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { meta, err := pp.place.GetMeta(ctx, zid) if err != nil { return err } - user := session.GetUser(ctx) + user := pp.auth.GetUser(ctx) if pp.policy.CanDelete(user, meta) { return pp.place.DeleteZettel(ctx, zid) } return place.NewErrNotAllowed("Delete", user, zid) } func (pp *polPlace) ReadStats(st *place.Stats) { pp.place.ReadStats(st) } + +func (pp *polPlace) Dump(w io.Writer) { + pp.place.Dump(w) +} Index: auth/policy/policy.go ================================================================== --- auth/policy/policy.go +++ auth/policy/policy.go @@ -10,67 +10,40 @@ // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( - "zettelstore.de/z/domain/id" + "zettelstore.de/z/auth" + "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" ) -// Policy is an interface for checking access authorization. -type Policy interface { - // User is allowed to create a new zettel. - CanCreate(user, newMeta *meta.Meta) bool - - // User is allowed to read zettel - CanRead(user, m *meta.Meta) bool - - // User is allowed to write zettel. - CanWrite(user, oldMeta, newMeta *meta.Meta) bool - - // User is allowed to rename zettel - CanRename(user, m *meta.Meta) bool - - // User is allowed to delete zettel - CanDelete(user, m *meta.Meta) bool -} - // newPolicy creates a policy based on given constraints. -func newPolicy( - simpleMode bool, - withAuth func() bool, - isReadOnlyMode bool, - expertMode func() bool, - isOwner func(id.Zid) bool, - getVisibility func(*meta.Meta) meta.Visibility, -) Policy { - var pol Policy - if isReadOnlyMode { +func newPolicy(manager auth.AuthzManager, authConfig config.AuthConfig) auth.Policy { + var pol auth.Policy + if manager.IsReadonly() { pol = &roPolicy{} } else { - pol = &defaultPolicy{} + pol = &defaultPolicy{manager} } - if withAuth() { + if manager.WithAuth() { pol = &ownerPolicy{ - expertMode: expertMode, - isOwner: isOwner, - getVisibility: getVisibility, - pre: pol, + manager: manager, + authConfig: authConfig, + pre: pol, } } else { pol = &anonPolicy{ - simpleMode: simpleMode, - expertMode: expertMode, - getVisibility: getVisibility, - pre: pol, + authConfig: authConfig, + pre: pol, } } return &prePolicy{pol} } type prePolicy struct { - post Policy + post auth.Policy } func (p *prePolicy) CanCreate(user, newMeta *meta.Meta) bool { return newMeta != nil && p.post.CanCreate(user, newMeta) } Index: auth/policy/policy_test.go ================================================================== --- auth/policy/policy_test.go +++ auth/policy/policy_test.go @@ -13,86 +13,96 @@ import ( "fmt" "testing" + "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestPolicies(t *testing.T) { testScene := []struct { - simple bool readonly bool withAuth bool expert bool }{ - {true, true, true, true}, - {true, true, true, false}, - {true, true, false, true}, - {true, true, false, false}, - {true, false, true, true}, - {true, false, true, false}, - {true, false, false, true}, - {true, false, false, false}, - {false, true, true, true}, - {false, true, true, false}, - {false, true, false, true}, - {false, true, false, false}, - {false, false, true, true}, - {false, false, true, false}, - {false, false, false, true}, - {false, false, false, false}, + {true, true, true}, + {true, true, false}, + {true, false, true}, + {true, false, false}, + {false, true, true}, + {false, true, false}, + {false, false, true}, + {false, false, false}, } for _, ts := range testScene { - var authFunc func() bool - if ts.withAuth { - authFunc = withAuth - } else { - authFunc = withoutAuth - } - var expertFunc func() bool - if ts.expert { - expertFunc = expertMode - } else { - expertFunc = noExpertMode - } - pol := newPolicy(ts.simple, authFunc, ts.readonly, expertFunc, isOwner, getVisibility) - name := fmt.Sprintf("simple=%v/readonly=%v/withauth=%v/expert=%v", - ts.simple, ts.readonly, ts.withAuth, ts.expert) - t.Run(name, func(tt *testing.T) { - testCreate(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) - testRead(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) - testWrite(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) - testRename(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) - testDelete(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) + authzManager := &testAuthzManager{ + readOnly: ts.readonly, + withAuth: ts.withAuth, + } + pol := newPolicy(authzManager, &authConfig{ts.expert}) + name := fmt.Sprintf("readonly=%v/withauth=%v/expert=%v", + ts.readonly, ts.withAuth, ts.expert) + t.Run(name, func(tt *testing.T) { + testCreate(tt, pol, ts.withAuth, ts.readonly, ts.expert) + testRead(tt, pol, ts.withAuth, ts.readonly, ts.expert) + testWrite(tt, pol, ts.withAuth, ts.readonly, ts.expert) + testRename(tt, pol, ts.withAuth, ts.readonly, ts.expert) + testDelete(tt, pol, ts.withAuth, ts.readonly, ts.expert) }) } } -func withAuth() bool { return true } -func withoutAuth() bool { return false } -func expertMode() bool { return true } -func noExpertMode() bool { return false } -func isOwner(zid id.Zid) bool { return zid == ownerZid } -func getVisibility(m *meta.Meta) meta.Visibility { +type testAuthzManager struct { + readOnly bool + withAuth bool +} + +func (a *testAuthzManager) IsReadonly() bool { return a.readOnly } +func (a *testAuthzManager) Owner() id.Zid { return ownerZid } +func (a *testAuthzManager) IsOwner(zid id.Zid) bool { return zid == ownerZid } + +func (a *testAuthzManager) WithAuth() bool { return a.withAuth } + +func (a *testAuthzManager) GetUserRole(user *meta.Meta) meta.UserRole { + if user == nil { + if a.WithAuth() { + return meta.UserRoleUnknown + } + return meta.UserRoleOwner + } + if a.IsOwner(user.Zid) { + return meta.UserRoleOwner + } + if val, ok := user.Get(meta.KeyUserRole); ok { + if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { + return ur + } + } + return meta.UserRoleReader +} + +type authConfig struct{ expert bool } + +func (ac *authConfig) GetExpertMode() bool { return ac.expert } + +func (ac *authConfig) GetVisibility(m *meta.Meta) meta.Visibility { if vis, ok := m.Get(meta.KeyVisibility); ok { switch vis { case meta.ValueVisibilityPublic: return meta.VisibilityPublic case meta.ValueVisibilityOwner: return meta.VisibilityOwner case meta.ValueVisibilityExpert: return meta.VisibilityExpert - case meta.ValueVisibilitySimple: - return meta.VisibilitySimple } } return meta.VisibilityLogin } -func testCreate(t *testing.T, pol Policy, simple, withAuth, readonly, isExpert bool) { +func testCreate(t *testing.T, pol auth.Policy, withAuth, readonly, isExpert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() @@ -131,11 +141,11 @@ } }) } } -func testRead(t *testing.T, pol Policy, simple, withAuth, readonly, expert bool) { +func testRead(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() @@ -143,11 +153,10 @@ zettel := newZettel() publicZettel := newPublicZettel() loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() - simpleZettel := newSimpleZettel() userZettel := newUserZettel() testCases := []struct { user *meta.Meta meta *meta.Meta exp bool @@ -186,16 +195,10 @@ {anonUser, expertZettel, !withAuth && expert}, {reader, expertZettel, !withAuth && expert}, {writer, expertZettel, !withAuth && expert}, {owner, expertZettel, expert}, {owner2, expertZettel, expert}, - // Simple expert zettel - {anonUser, simpleZettel, !withAuth && (simple || expert)}, - {reader, simpleZettel, !withAuth && (simple || expert)}, - {writer, simpleZettel, !withAuth && (simple || expert)}, - {owner, simpleZettel, (!withAuth && simple) || expert}, - {owner2, simpleZettel, (!withAuth && simple) || expert}, // Other user zettel {anonUser, userZettel, !withAuth}, {reader, userZettel, !withAuth}, {writer, userZettel, !withAuth}, {owner, userZettel, true}, @@ -216,11 +219,11 @@ } }) } } -func testWrite(t *testing.T, pol Policy, simple, withAuth, readonly, expert bool) { +func testWrite(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() @@ -228,19 +231,19 @@ zettel := newZettel() publicZettel := newPublicZettel() loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() - simpleZettel := newSimpleZettel() userZettel := newUserZettel() writerNew := writer.Clone() writerNew.Set(meta.KeyUserRole, owner.GetDefault(meta.KeyUserRole, "")) roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() + notAuthNotReadonly := !withAuth && !readonly testCases := []struct { user *meta.Meta old *meta.Meta new *meta.Meta exp bool @@ -268,61 +271,55 @@ {reader, zettel, publicZettel, false}, {writer, zettel, publicZettel, false}, {owner, zettel, publicZettel, false}, {owner2, zettel, publicZettel, false}, // Overwrite a normal zettel - {anonUser, zettel, zettel, !withAuth && !readonly}, - {reader, zettel, zettel, !withAuth && !readonly}, + {anonUser, zettel, zettel, notAuthNotReadonly}, + {reader, zettel, zettel, notAuthNotReadonly}, {writer, zettel, zettel, !readonly}, {owner, zettel, zettel, !readonly}, {owner2, zettel, zettel, !readonly}, // Public zettel - {anonUser, publicZettel, publicZettel, !withAuth && !readonly}, - {reader, publicZettel, publicZettel, !withAuth && !readonly}, + {anonUser, publicZettel, publicZettel, notAuthNotReadonly}, + {reader, publicZettel, publicZettel, notAuthNotReadonly}, {writer, publicZettel, publicZettel, !readonly}, {owner, publicZettel, publicZettel, !readonly}, {owner2, publicZettel, publicZettel, !readonly}, // Login zettel - {anonUser, loginZettel, loginZettel, !withAuth && !readonly}, - {reader, loginZettel, loginZettel, !withAuth && !readonly}, + {anonUser, loginZettel, loginZettel, notAuthNotReadonly}, + {reader, loginZettel, loginZettel, notAuthNotReadonly}, {writer, loginZettel, loginZettel, !readonly}, {owner, loginZettel, loginZettel, !readonly}, {owner2, loginZettel, loginZettel, !readonly}, // Owner zettel - {anonUser, ownerZettel, ownerZettel, !withAuth && !readonly}, - {reader, ownerZettel, ownerZettel, !withAuth && !readonly}, - {writer, ownerZettel, ownerZettel, !withAuth && !readonly}, + {anonUser, ownerZettel, ownerZettel, notAuthNotReadonly}, + {reader, ownerZettel, ownerZettel, notAuthNotReadonly}, + {writer, ownerZettel, ownerZettel, notAuthNotReadonly}, {owner, ownerZettel, ownerZettel, !readonly}, {owner2, ownerZettel, ownerZettel, !readonly}, // Expert zettel - {anonUser, expertZettel, expertZettel, !withAuth && !readonly && expert}, - {reader, expertZettel, expertZettel, !withAuth && !readonly && expert}, - {writer, expertZettel, expertZettel, !withAuth && !readonly && expert}, + {anonUser, expertZettel, expertZettel, notAuthNotReadonly && expert}, + {reader, expertZettel, expertZettel, notAuthNotReadonly && expert}, + {writer, expertZettel, expertZettel, notAuthNotReadonly && expert}, {owner, expertZettel, expertZettel, !readonly && expert}, {owner2, expertZettel, expertZettel, !readonly && expert}, - // Simple expert zettel - {anonUser, simpleZettel, expertZettel, !withAuth && !readonly && (simple || expert)}, - {reader, simpleZettel, expertZettel, !withAuth && !readonly && (simple || expert)}, - {writer, simpleZettel, expertZettel, !withAuth && !readonly && (simple || expert)}, - {owner, simpleZettel, expertZettel, !readonly && ((!withAuth && simple) || expert)}, - {owner2, simpleZettel, expertZettel, !readonly && ((!withAuth && simple) || expert)}, // Other user zettel - {anonUser, userZettel, userZettel, !withAuth && !readonly}, - {reader, userZettel, userZettel, !withAuth && !readonly}, - {writer, userZettel, userZettel, !withAuth && !readonly}, + {anonUser, userZettel, userZettel, notAuthNotReadonly}, + {reader, userZettel, userZettel, notAuthNotReadonly}, + {writer, userZettel, userZettel, notAuthNotReadonly}, {owner, userZettel, userZettel, !readonly}, {owner2, userZettel, userZettel, !readonly}, // Own user zettel {reader, reader, reader, !readonly}, {writer, writer, writer, !readonly}, {owner, owner, owner, !readonly}, {owner2, owner2, owner2, !readonly}, // Writer cannot change importand metadata of its own user zettel - {writer, writer, writerNew, !withAuth && !readonly}, + {writer, writer, writerNew, notAuthNotReadonly}, // No r/o zettel - {anonUser, roFalse, roFalse, !withAuth && !readonly}, - {reader, roFalse, roFalse, !withAuth && !readonly}, + {anonUser, roFalse, roFalse, notAuthNotReadonly}, + {reader, roFalse, roFalse, notAuthNotReadonly}, {writer, roFalse, roFalse, !readonly}, {owner, roFalse, roFalse, !readonly}, {owner2, roFalse, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, roReader, false}, @@ -357,25 +354,25 @@ } }) } } -func testRename(t *testing.T, pol Policy, simple, withAuth, readonly, expert bool) { +func testRename(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() expertZettel := newExpertZettel() - simpleZettel := newSimpleZettel() roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() + notAuthNotReadonly := !withAuth && !readonly testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ @@ -384,37 +381,31 @@ {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Any zettel - {anonUser, zettel, !withAuth && !readonly}, - {reader, zettel, !withAuth && !readonly}, - {writer, zettel, !withAuth && !readonly}, + {anonUser, zettel, notAuthNotReadonly}, + {reader, zettel, notAuthNotReadonly}, + {writer, zettel, notAuthNotReadonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // Expert zettel - {anonUser, expertZettel, !withAuth && !readonly && expert}, - {reader, expertZettel, !withAuth && !readonly && expert}, - {writer, expertZettel, !withAuth && !readonly && expert}, + {anonUser, expertZettel, notAuthNotReadonly && expert}, + {reader, expertZettel, notAuthNotReadonly && expert}, + {writer, expertZettel, notAuthNotReadonly && expert}, {owner, expertZettel, !readonly && expert}, {owner2, expertZettel, !readonly && expert}, - // Simple expert zettel - {anonUser, simpleZettel, !withAuth && !readonly && (simple || expert)}, - {reader, simpleZettel, !withAuth && !readonly && (simple || expert)}, - {writer, simpleZettel, !withAuth && !readonly && (simple || expert)}, - {owner, simpleZettel, !readonly && ((!withAuth && simple) || expert)}, - {owner2, simpleZettel, !readonly && ((!withAuth && simple) || expert)}, // No r/o zettel - {anonUser, roFalse, !withAuth && !readonly}, - {reader, roFalse, !withAuth && !readonly}, - {writer, roFalse, !withAuth && !readonly}, + {anonUser, roFalse, notAuthNotReadonly}, + {reader, roFalse, notAuthNotReadonly}, + {writer, roFalse, notAuthNotReadonly}, {owner, roFalse, !readonly}, {owner2, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, false}, {reader, roReader, false}, - {writer, roReader, !withAuth && !readonly}, + {writer, roReader, notAuthNotReadonly}, {owner, roReader, !readonly}, {owner2, roReader, !readonly}, // Writer r/o zettel {anonUser, roWriter, false}, {reader, roWriter, false}, @@ -442,25 +433,25 @@ } }) } } -func testDelete(t *testing.T, pol Policy, simple, withAuth, readonly, expert bool) { +func testDelete(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() expertZettel := newExpertZettel() - simpleZettel := newSimpleZettel() roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() + notAuthNotReadonly := !withAuth && !readonly testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ @@ -469,37 +460,31 @@ {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Any zettel - {anonUser, zettel, !withAuth && !readonly}, - {reader, zettel, !withAuth && !readonly}, - {writer, zettel, !withAuth && !readonly}, + {anonUser, zettel, notAuthNotReadonly}, + {reader, zettel, notAuthNotReadonly}, + {writer, zettel, notAuthNotReadonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // Expert zettel - {anonUser, expertZettel, !withAuth && !readonly && expert}, - {reader, expertZettel, !withAuth && !readonly && expert}, - {writer, expertZettel, !withAuth && !readonly && expert}, + {anonUser, expertZettel, notAuthNotReadonly && expert}, + {reader, expertZettel, notAuthNotReadonly && expert}, + {writer, expertZettel, notAuthNotReadonly && expert}, {owner, expertZettel, !readonly && expert}, {owner2, expertZettel, !readonly && expert}, - // Simple expert zettel - {anonUser, simpleZettel, !withAuth && !readonly && (simple || expert)}, - {reader, simpleZettel, !withAuth && !readonly && (simple || expert)}, - {writer, simpleZettel, !withAuth && !readonly && (simple || expert)}, - {owner, simpleZettel, !readonly && ((!withAuth && simple) || expert)}, - {owner2, simpleZettel, !readonly && ((!withAuth && simple) || expert)}, // No r/o zettel - {anonUser, roFalse, !withAuth && !readonly}, - {reader, roFalse, !withAuth && !readonly}, - {writer, roFalse, !withAuth && !readonly}, + {anonUser, roFalse, notAuthNotReadonly}, + {reader, roFalse, notAuthNotReadonly}, + {writer, roFalse, notAuthNotReadonly}, {owner, roFalse, !readonly}, {owner2, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, false}, {reader, roReader, false}, - {writer, roReader, !withAuth && !readonly}, + {writer, roReader, notAuthNotReadonly}, {owner, roReader, !readonly}, {owner2, roReader, !readonly}, // Writer r/o zettel {anonUser, roWriter, false}, {reader, roWriter, false}, @@ -595,16 +580,10 @@ m := meta.New(visZid) m.Set(meta.KeyTitle, "Expert Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } -func newSimpleZettel() *meta.Meta { - m := meta.New(visZid) - m.Set(meta.KeyTitle, "Simple Zettel") - m.Set(meta.KeyVisibility, meta.ValueVisibilitySimple) - return m -} func newRoFalseZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "No r/o Zettel") m.Set(meta.KeyReadOnly, "false") return m DELETED auth/token/token.go Index: auth/token/token.go ================================================================== --- auth/token/token.go +++ auth/token/token.go @@ -1,128 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package token provides some function for handling auth token. -package token - -import ( - "errors" - "time" - - "github.com/pascaldekloe/jwt" - - "zettelstore.de/z/config/startup" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" -) - -const reqHash = jwt.HS512 - -// ErrNoUser signals that the meta data has no role value 'user'. -var ErrNoUser = errors.New("auth: meta is no user") - -// ErrNoIdent signals that the 'ident' key is missing. -var ErrNoIdent = errors.New("auth: missing ident") - -// ErrOtherKind signals that the token was defined for another token kind. -var ErrOtherKind = errors.New("auth: wrong token kind") - -// ErrNoZid signals that the 'zid' key is missing. -var ErrNoZid = errors.New("auth: missing zettel id") - -// Kind specifies for which application / usage a token is/was requested. -type Kind int - -// Allowed values of token kind -const ( - _ Kind = iota - KindJSON - KindHTML -) - -// GetToken returns a token to be used for authentification. -func GetToken(ident *meta.Meta, d time.Duration, kind Kind) ([]byte, error) { - if role, ok := ident.Get(meta.KeyRole); !ok || role != meta.ValueRoleUser { - return nil, ErrNoUser - } - subject, ok := ident.Get(meta.KeyUserID) - if !ok || subject == "" { - return nil, ErrNoIdent - } - - now := time.Now().Round(time.Second) - claims := jwt.Claims{ - Registered: jwt.Registered{ - Subject: subject, - Expires: jwt.NewNumericTime(now.Add(d)), - Issued: jwt.NewNumericTime(now), - }, - Set: map[string]interface{}{ - "zid": ident.Zid.String(), - "_tk": int(kind), - }, - } - token, err := claims.HMACSign(reqHash, startup.Secret()) - if err != nil { - return nil, err - } - return token, nil -} - -// ErrTokenExpired signals an exired token -var ErrTokenExpired = errors.New("auth: token expired") - -// Data contains some important elements from a token. -type Data struct { - Token []byte - Now time.Time - Issued time.Time - Expires time.Time - Ident string - Zid id.Zid -} - -// CheckToken checks the validity of the token and returns relevant data. -func CheckToken(token []byte, k Kind) (Data, error) { - h, err := jwt.NewHMAC(reqHash, startup.Secret()) - if err != nil { - return Data{}, err - } - claims, err := h.Check(token) - if err != nil { - return Data{}, err - } - now := time.Now().Round(time.Second) - expires := claims.Expires.Time() - if expires.Before(now) { - return Data{}, ErrTokenExpired - } - ident := claims.Subject - if ident == "" { - return Data{}, ErrNoIdent - } - if zidS, ok := claims.Set["zid"].(string); ok { - if zid, err := id.Parse(zidS); err == nil { - if kind, ok := claims.Set["_tk"].(float64); ok { - if Kind(kind) == k { - return Data{ - Token: token, - Now: now, - Issued: claims.Issued.Time(), - Expires: expires, - Ident: ident, - Zid: zid, - }, nil - } - } - return Data{}, ErrOtherKind - } - } - return Data{}, ErrNoZid -} DELETED cmd/cmd_config.go Index: cmd/cmd_config.go ================================================================== --- cmd/cmd_config.go +++ cmd/cmd_config.go @@ -1,40 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -package cmd - -import ( - "flag" - "fmt" - - "zettelstore.de/z/config/startup" -) - -// ---------- Subcommand: config --------------------------------------------- - -func cmdConfig(*flag.FlagSet) (int, error) { - fmtVersion() - fmt.Println("Stores") - fmt.Printf(" Read-only mode = %v\n", startup.IsReadOnlyMode()) - fmt.Println("Web") - fmt.Printf(" Listen address = %q\n", startup.ListenAddress()) - fmt.Printf(" URL prefix = %q\n", startup.URLPrefix()) - if startup.WithAuth() { - fmt.Println("Auth") - fmt.Printf(" Owner = %v\n", startup.Owner()) - fmt.Printf(" Secure cookie = %v\n", startup.SecureCookie()) - fmt.Printf(" Persistent cookie = %v\n", startup.PersistentCookie()) - htmlLifetime, apiLifetime := startup.TokenLifetime() - fmt.Printf(" HTML lifetime = %v\n", htmlLifetime) - fmt.Printf(" API lifetime = %v\n", apiLifetime) - } - - return 0, nil -} Index: cmd/cmd_file.go ================================================================== --- cmd/cmd_file.go +++ cmd/cmd_file.go @@ -14,11 +14,10 @@ "flag" "fmt" "io" "os" - "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" @@ -25,24 +24,25 @@ "zettelstore.de/z/parser" ) // ---------- Subcommand: file ----------------------------------------------- -func cmdFile(fs *flag.FlagSet) (int, error) { +func cmdFile(fs *flag.FlagSet, cfg *meta.Meta) (int, error) { format := fs.Lookup("t").Value.String() - meta, inp, err := getInput(fs.Args()) - if meta == nil { + m, inp, err := getInput(fs.Args()) + if m == nil { return 2, err } z := parser.ParseZettel( domain.Zettel{ - Meta: meta, + Meta: m, Content: domain.NewContent(inp.Src[inp.Pos:]), }, - runtime.GetSyntax(meta), + m.GetDefault(meta.KeySyntax, meta.ValueSyntaxZmk), + nil, ) - enc := encoder.Create(format, &encoder.Environment{Lang: runtime.GetLang(meta)}) + enc := encoder.Create(format, &encoder.Environment{Lang: m.GetDefault(meta.KeyLang, meta.ValueLangEN)}) if enc == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", format) return 2, nil } _, err = enc.WriteZettel(os.Stdout, z, format != "raw") Index: cmd/cmd_password.go ================================================================== --- cmd/cmd_password.go +++ cmd/cmd_password.go @@ -22,11 +22,11 @@ "zettelstore.de/z/domain/meta" ) // ---------- Subcommand: password ------------------------------------------- -func cmdPassword(fs *flag.FlagSet) (int, error) { +func cmdPassword(fs *flag.FlagSet, cfg *meta.Meta) (int, error) { if fs.NArg() == 0 { fmt.Fprintln(os.Stderr, "User name and user zettel identification missing") return 2, nil } if fs.NArg() == 1 { Index: cmd/cmd_run.go ================================================================== --- cmd/cmd_run.go +++ cmd/cmd_run.go @@ -10,134 +10,117 @@ package cmd import ( "flag" - "log" "net/http" - "zettelstore.de/z/auth/policy" - "zettelstore.de/z/config/runtime" - "zettelstore.de/z/config/startup" + "zettelstore.de/z/auth" + "zettelstore.de/z/config" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/kernel" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/adapter/api" "zettelstore.de/z/web/adapter/webui" - "zettelstore.de/z/web/router" "zettelstore.de/z/web/server" - "zettelstore.de/z/web/session" ) // ---------- Subcommand: run ------------------------------------------------ func flgRun(fs *flag.FlagSet) { fs.String("c", defConfigfile, "configuration file") - fs.Uint("p", 23123, "port number") + fs.Uint("a", 0, "port number kernel service (0=disable)") + fs.Uint("p", 23123, "port number web service") fs.String("d", "", "zettel directory") fs.Bool("r", false, "system-wide read-only mode") fs.Bool("v", false, "verbose mode") fs.Bool("debug", false, "debug mode") } -func enableDebug(fs *flag.FlagSet, srv *server.Server) { - if dbg := fs.Lookup("debug"); dbg != nil && dbg.Value.String() == "true" { - srv.SetDebug() - } +func withDebug(fs *flag.FlagSet) bool { + dbg := fs.Lookup("debug") + return dbg != nil && dbg.Value.String() == "true" +} + +func runFunc(fs *flag.FlagSet, cfg *meta.Meta) (int, error) { + exitCode, err := doRun(withDebug(fs)) + kernel.Main.WaitForShutdown() + return exitCode, err } -func runFunc(fs *flag.FlagSet) (int, error) { - listenAddr := startup.ListenAddress() - readonlyMode := startup.IsReadOnlyMode() - logBeforeRun(listenAddr, readonlyMode) - handler := setupRouting(startup.PlaceManager(), readonlyMode) - srv := server.New(listenAddr, handler) - enableDebug(fs, srv) - if err := srv.Run(); err != nil { +func doRun(debug bool) (int, error) { + kern := kernel.Main + kern.SetDebug(debug) + if err := kern.StartService(kernel.WebService); err != nil { return 1, err } return 0, nil } -func logBeforeRun(listenAddr string, readonlyMode bool) { - v := startup.GetVersion() - log.Printf("%v %v (%v@%v/%v)", v.Prog, v.Build, v.GoVersion, v.Os, v.Arch) - log.Println("Licensed under the latest version of the EUPL (European Union Public License)") - log.Printf("Listening on %v", listenAddr) - log.Printf("Zettel location [%v]", startup.PlaceManager().Location()) - if readonlyMode { - log.Println("Read-only mode") - } -} - -func setupRouting(mgr place.Manager, readonlyMode bool) http.Handler { - var up place.Place = mgr - pp, pol := policy.PlaceWithPolicy( - up, startup.IsSimple(), startup.WithAuth, readonlyMode, runtime.GetExpertMode, - startup.IsOwner, runtime.GetVisibility) - te := webui.NewTemplateEngine(mgr, pol) - - ucAuthenticate := usecase.NewAuthenticate(up) - ucGetMeta := usecase.NewGetMeta(pp) - ucGetZettel := usecase.NewGetZettel(pp) - ucParseZettel := usecase.NewParseZettel(ucGetZettel) - ucListMeta := usecase.NewListMeta(pp) - ucListRoles := usecase.NewListRole(pp) - ucListTags := usecase.NewListTags(pp) - ucZettelContext := usecase.NewZettelContext(pp) - - router := router.NewRouter() - router.Handle("/", webui.MakeGetRootHandler(te, pp)) - router.AddListRoute('a', http.MethodGet, webui.MakeGetLoginHandler(te)) - router.AddListRoute('a', http.MethodPost, adapter.MakePostLoginHandler( +func setupRouting(webSrv server.Server, placeManager place.Manager, authManager auth.Manager, rtConfig config.Config) { + protectedPlaceManager, authPolicy := authManager.PlaceWithPolicy(webSrv, placeManager, rtConfig) + api := api.New(webSrv, authManager, authManager, webSrv, rtConfig) + wui := webui.New(webSrv, authManager, rtConfig, authManager, placeManager, authPolicy) + + ucAuthenticate := usecase.NewAuthenticate(authManager, authManager, placeManager) + ucCreateZettel := usecase.NewCreateZettel(rtConfig, protectedPlaceManager) + ucGetMeta := usecase.NewGetMeta(protectedPlaceManager) + ucGetZettel := usecase.NewGetZettel(protectedPlaceManager) + ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) + ucListMeta := usecase.NewListMeta(protectedPlaceManager) + ucListRoles := usecase.NewListRole(protectedPlaceManager) + ucListTags := usecase.NewListTags(protectedPlaceManager) + ucZettelContext := usecase.NewZettelContext(protectedPlaceManager) + + webSrv.Handle("/", wui.MakeGetRootHandler(protectedPlaceManager)) + webSrv.AddListRoute('a', http.MethodGet, wui.MakeGetLoginHandler()) + webSrv.AddListRoute('a', http.MethodPost, adapter.MakePostLoginHandler( api.MakePostLoginHandlerAPI(ucAuthenticate), - webui.MakePostLoginHandlerHTML(te, ucAuthenticate))) - router.AddListRoute('a', http.MethodPut, api.MakeRenewAuthHandler()) - router.AddZettelRoute('a', http.MethodGet, webui.MakeGetLogoutHandler(te)) - if !readonlyMode { - router.AddZettelRoute('b', http.MethodGet, webui.MakeGetRenameZettelHandler( - te, ucGetMeta)) - router.AddZettelRoute('b', http.MethodPost, webui.MakePostRenameZettelHandler( - te, usecase.NewRenameZettel(pp))) - router.AddZettelRoute('c', http.MethodGet, webui.MakeGetCopyZettelHandler( - te, ucGetZettel, usecase.NewCopyZettel())) - router.AddZettelRoute('c', http.MethodPost, webui.MakePostCreateZettelHandler( - te, usecase.NewCreateZettel(pp))) - router.AddZettelRoute('d', http.MethodGet, webui.MakeGetDeleteZettelHandler( - te, ucGetZettel)) - router.AddZettelRoute('d', http.MethodPost, webui.MakePostDeleteZettelHandler( - te, usecase.NewDeleteZettel(pp))) - router.AddZettelRoute('e', http.MethodGet, webui.MakeEditGetZettelHandler( - te, ucGetZettel)) - router.AddZettelRoute('e', http.MethodPost, webui.MakeEditSetZettelHandler( - te, usecase.NewUpdateZettel(pp))) - router.AddZettelRoute('f', http.MethodGet, webui.MakeGetFolgeZettelHandler( - te, ucGetZettel, usecase.NewFolgeZettel())) - router.AddZettelRoute('f', http.MethodPost, webui.MakePostCreateZettelHandler( - te, usecase.NewCreateZettel(pp))) - router.AddZettelRoute('g', http.MethodGet, webui.MakeGetNewZettelHandler( - te, ucGetZettel, usecase.NewNewZettel())) - router.AddZettelRoute('g', http.MethodPost, webui.MakePostCreateZettelHandler( - te, usecase.NewCreateZettel(pp))) - } - router.AddListRoute('f', http.MethodGet, webui.MakeSearchHandler( - te, usecase.NewSearch(pp), ucGetMeta, ucGetZettel)) - router.AddListRoute('h', http.MethodGet, webui.MakeListHTMLMetaHandler( - te, ucListMeta, ucListRoles, ucListTags)) - router.AddZettelRoute('h', http.MethodGet, webui.MakeGetHTMLZettelHandler( - te, ucParseZettel, ucGetMeta)) - router.AddZettelRoute('i', http.MethodGet, webui.MakeGetInfoHandler( - te, ucParseZettel, ucGetMeta)) - router.AddZettelRoute('j', http.MethodGet, webui.MakeZettelContextHandler(te, ucZettelContext)) - - router.AddZettelRoute('l', http.MethodGet, api.MakeGetLinksHandler(ucParseZettel)) - router.AddZettelRoute('o', http.MethodGet, api.MakeGetOrderHandler( - usecase.NewZettelOrder(pp, ucParseZettel))) - router.AddListRoute('r', http.MethodGet, api.MakeListRoleHandler(ucListRoles)) - router.AddListRoute('t', http.MethodGet, api.MakeListTagsHandler(ucListTags)) - router.AddZettelRoute('y', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext)) - router.AddListRoute('z', http.MethodGet, api.MakeListMetaHandler( - usecase.NewListMeta(pp), ucGetMeta, ucParseZettel)) - router.AddZettelRoute('z', http.MethodGet, api.MakeGetZettelHandler( - ucParseZettel, ucGetMeta)) - return session.NewHandler(router, usecase.NewGetUserByZid(up)) + wui.MakePostLoginHandlerHTML(ucAuthenticate))) + webSrv.AddListRoute('a', http.MethodPut, api.MakeRenewAuthHandler()) + webSrv.AddZettelRoute('a', http.MethodGet, wui.MakeGetLogoutHandler()) + if !authManager.IsReadonly() { + webSrv.AddZettelRoute('b', http.MethodGet, wui.MakeGetRenameZettelHandler(ucGetMeta)) + webSrv.AddZettelRoute('b', http.MethodPost, wui.MakePostRenameZettelHandler( + usecase.NewRenameZettel(protectedPlaceManager))) + webSrv.AddZettelRoute('c', http.MethodGet, wui.MakeGetCopyZettelHandler( + ucGetZettel, usecase.NewCopyZettel())) + webSrv.AddZettelRoute('c', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) + webSrv.AddZettelRoute('d', http.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel)) + webSrv.AddZettelRoute('d', http.MethodPost, wui.MakePostDeleteZettelHandler( + usecase.NewDeleteZettel(protectedPlaceManager))) + webSrv.AddZettelRoute('e', http.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel)) + webSrv.AddZettelRoute('e', http.MethodPost, wui.MakeEditSetZettelHandler( + usecase.NewUpdateZettel(protectedPlaceManager))) + webSrv.AddZettelRoute('f', http.MethodGet, wui.MakeGetFolgeZettelHandler( + ucGetZettel, usecase.NewFolgeZettel(rtConfig))) + webSrv.AddZettelRoute('f', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) + webSrv.AddZettelRoute('g', http.MethodGet, wui.MakeGetNewZettelHandler( + ucGetZettel, usecase.NewNewZettel())) + webSrv.AddZettelRoute('g', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) + } + webSrv.AddListRoute('f', http.MethodGet, wui.MakeSearchHandler( + usecase.NewSearch(protectedPlaceManager), ucGetMeta, ucGetZettel)) + webSrv.AddListRoute('h', http.MethodGet, wui.MakeListHTMLMetaHandler( + ucListMeta, ucListRoles, ucListTags)) + webSrv.AddZettelRoute('h', http.MethodGet, wui.MakeGetHTMLZettelHandler( + ucParseZettel, ucGetMeta)) + webSrv.AddZettelRoute('i', http.MethodGet, wui.MakeGetInfoHandler(ucParseZettel, ucGetMeta)) + webSrv.AddZettelRoute('j', http.MethodGet, wui.MakeZettelContextHandler(ucZettelContext)) + + webSrv.AddZettelRoute('l', http.MethodGet, api.MakeGetLinksHandler(ucParseZettel)) + webSrv.AddZettelRoute('o', http.MethodGet, api.MakeGetOrderHandler( + usecase.NewZettelOrder(protectedPlaceManager, ucParseZettel))) + webSrv.AddListRoute('r', http.MethodGet, api.MakeListRoleHandler(ucListRoles)) + webSrv.AddListRoute('t', http.MethodGet, api.MakeListTagsHandler(ucListTags)) + webSrv.AddZettelRoute('y', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext)) + webSrv.AddListRoute('z', http.MethodGet, api.MakeListMetaHandler( + usecase.NewListMeta(protectedPlaceManager), ucGetMeta, ucParseZettel)) + webSrv.AddZettelRoute('z', http.MethodGet, api.MakeGetZettelHandler( + ucParseZettel, ucGetMeta)) + + if authManager.WithAuth() { + webSrv.SetUserRetriever(usecase.NewGetUserByZid(placeManager)) + } } Index: cmd/cmd_run_simple.go ================================================================== --- cmd/cmd_run_simple.go +++ cmd/cmd_run_simple.go @@ -11,47 +11,42 @@ package cmd import ( "flag" "fmt" - "log" "os" "strings" - "zettelstore.de/z/config/startup" - "zettelstore.de/z/web/server" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/kernel" ) func flgSimpleRun(fs *flag.FlagSet) { fs.String("d", "", "zettel directory") } -func runSimpleFunc(*flag.FlagSet) (int, error) { - listenAddr := startup.ListenAddress() - readonlyMode := startup.IsReadOnlyMode() - logBeforeRun(listenAddr, readonlyMode) - if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 { - log.Println() - log.Println("--------------------------") - log.Printf("Open your browser and enter the following URL:") - log.Println() - log.Printf(" http://localhost%v", listenAddr[idx:]) - } - - handler := setupRouting(startup.PlaceManager(), readonlyMode) - srv := server.New(listenAddr, handler) - if err := srv.Run(); err != nil { - return 1, err - } - return 0, nil +func runSimpleFunc(fs *flag.FlagSet, cfg *meta.Meta) (int, error) { + kern := kernel.Main + listenAddr := kern.GetConfig(kernel.WebService, kernel.WebListenAddress).(string) + exitCode, err := doRun(false) + if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 { + kern.Log() + kern.Log("--------------------------") + kern.Log("Open your browser and enter the following URL:") + kern.Log() + kern.Log(fmt.Sprintf(" http://localhost%v", listenAddr[idx:])) + kern.Log() + } + kern.WaitForShutdown() + return exitCode, err } // runSimple is called, when the user just starts the software via a double click // or via a simple call ``./zettelstore`` on the command line. -func runSimple() { +func runSimple() int { dir := "./zettel" if err := os.MkdirAll(dir, 0750); err != nil { fmt.Fprintf(os.Stderr, "Unable to create zettel directory %q (%s)\n", dir, err) os.Exit(1) } - executeCommand("run-simple", "-d", dir) + return executeCommand("run-simple", "-d", dir) } Index: cmd/command.go ================================================================== --- cmd/command.go +++ cmd/command.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020 Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -11,27 +11,29 @@ package cmd import ( "flag" "sort" + + "zettelstore.de/z/domain/meta" ) // Command stores information about commands / sub-commands. type Command struct { Name string // command name as it appears on the command line Func CommandFunc // function that executes a command Places bool // if true then places will be set up - Simple bool // Should start in simple mode + Header bool // Print a heading on startup Flags func(*flag.FlagSet) // function to set up flag.FlagSet flags *flag.FlagSet // flags that belong to the command } // CommandFunc is the function that executes the command. // It accepts the parsed command line parameters. // It returns the exit code and an error. -type CommandFunc func(*flag.FlagSet) (int, error) +type CommandFunc func(*flag.FlagSet, *meta.Meta) (int, error) // GetFlags return the flag.FlagSet defined for the command. func (c *Command) GetFlags() *flag.FlagSet { return c.flags } var commands = make(map[string]Command) Index: cmd/main.go ================================================================== --- cmd/main.go +++ cmd/main.go @@ -9,69 +9,66 @@ //----------------------------------------------------------------------------- package cmd import ( - "context" + "errors" "flag" "fmt" - "log" + "net" + "net/url" "os" + "strconv" "strings" - "zettelstore.de/z/config/runtime" - "zettelstore.de/z/config/startup" + "zettelstore.de/z/auth" + "zettelstore.de/z/auth/impl" + "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" - "zettelstore.de/z/index" - "zettelstore.de/z/index/indexer" "zettelstore.de/z/input" + "zettelstore.de/z/kernel" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" "zettelstore.de/z/place/progplace" + "zettelstore.de/z/web/server" ) const ( defConfigfile = ".zscfg" ) func init() { RegisterCommand(Command{ Name: "help", - Func: func(*flag.FlagSet) (int, error) { + Func: func(*flag.FlagSet, *meta.Meta) (int, error) { fmt.Println("Available commands:") for _, name := range List() { fmt.Printf("- %q\n", name) } return 0, nil }, }) RegisterCommand(Command{ - Name: "version", - Func: func(*flag.FlagSet) (int, error) { - fmtVersion() - return 0, nil - }, + Name: "version", + Func: func(*flag.FlagSet, *meta.Meta) (int, error) { return 0, nil }, + Header: true, }) RegisterCommand(Command{ Name: "run", Func: runFunc, Places: true, + Header: true, Flags: flgRun, }) RegisterCommand(Command{ Name: "run-simple", Func: runSimpleFunc, Places: true, - Simple: true, + Header: true, Flags: flgSimpleRun, }) - RegisterCommand(Command{ - Name: "config", - Func: cmdConfig, - Flags: flgRun, - }) RegisterCommand(Command{ Name: "file", Func: cmdFile, Flags: func(fs *flag.FlagSet) { fs.String("t", "html", "target output format") @@ -81,145 +78,190 @@ Name: "password", Func: cmdPassword, }) } -func fmtVersion() { - version := startup.GetVersion() - fmt.Printf("%v (%v/%v) running on %v (%v/%v)\n", - version.Prog, version.Build, version.GoVersion, - version.Hostname, version.Os, version.Arch) -} - -func getConfig(fs *flag.FlagSet) (cfg *meta.Meta) { +func readConfig(fs *flag.FlagSet) (cfg *meta.Meta) { var configFile string if configFlag := fs.Lookup("c"); configFlag != nil { configFile = configFlag.Value.String() } else { configFile = defConfigfile } content, err := os.ReadFile(configFile) if err != nil { - cfg = meta.New(id.Invalid) - } else { - cfg = meta.NewFromInput(id.Invalid, input.NewInput(string(content))) + return meta.New(id.Invalid) } + return meta.NewFromInput(id.Invalid, input.NewInput(string(content))) +} + +func getConfig(fs *flag.FlagSet) *meta.Meta { + cfg := readConfig(fs) fs.Visit(func(flg *flag.Flag) { switch flg.Name { case "p": - cfg.Set(startup.KeyListenAddress, "127.0.0.1:"+flg.Value.String()) + if portStr, err := parsePort(flg.Value.String()); err == nil { + cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", portStr)) + } + case "a": + if portStr, err := parsePort(flg.Value.String()); err == nil { + cfg.Set(keyAdminPort, portStr) + } case "d": val := flg.Value.String() if strings.HasPrefix(val, "/") { val = "dir://" + val } else { val = "dir:" + val } - cfg.Set(startup.KeyPlaceOneURI, val) + cfg.Set(keyPlaceOneURI, val) case "r": - cfg.Set(startup.KeyReadOnlyMode, flg.Value.String()) + cfg.Set(keyReadOnly, flg.Value.String()) case "v": - cfg.Set(startup.KeyVerbose, flg.Value.String()) + cfg.Set(keyVerbose, flg.Value.String()) } }) return cfg } -func setupOperations(cfg *meta.Meta, withPlaces, simple bool) error { - var mgr place.Manager - var idx index.Indexer - if withPlaces { - err := raiseFdLimit() - if err != nil { - log.Println("Raising some limitions did not work:", err) - log.Println("Prepare to encounter errors. Most of them can be mitigated. See the manual for details") - cfg.Set(startup.KeyDefaultDirPlaceType, startup.ValueDirPlaceTypeSimple) - } - startup.SetupStartupConfig(cfg) - idx = indexer.New() - filter := index.NewMetaFilter(idx) - mgr, err = manager.New(getPlaces(cfg), cfg.GetBool(startup.KeyReadOnlyMode), filter) - if err != nil { - return err - } - } else { - startup.SetupStartupConfig(cfg) - } - - startup.SetupStartupService(mgr, idx, simple) - if withPlaces { - if err := mgr.Start(context.Background()); err != nil { - fmt.Fprintln(os.Stderr, "Unable to start zettel place") - return err - } - runtime.SetupConfiguration(mgr) - progplace.Setup(cfg, mgr, idx) - idx.Start(mgr) - } - return nil -} - -func getPlaces(cfg *meta.Meta) []string { - var result []string = nil - for cnt := 1; ; cnt++ { - key := fmt.Sprintf("place-%v-uri", cnt) - uri, ok := cfg.Get(key) - if !ok || uri == "" { - if cnt > 1 { - break - } - uri = "dir:./zettel" - } - result = append(result, uri) - } - return result -} - -func cleanupOperations(withPlaces bool) error { - if withPlaces { - startup.Indexer().Stop() - if err := startup.PlaceManager().Stop(context.Background()); err != nil { - fmt.Fprintln(os.Stderr, "Unable to stop zettel place") - return err - } - } - return nil -} - -func executeCommand(name string, args ...string) { - command, ok := Get(name) - if !ok { - fmt.Fprintf(os.Stderr, "Unknown command %q\n", name) - os.Exit(1) - } - fs := command.GetFlags() - if err := fs.Parse(args); err != nil { - fmt.Fprintf(os.Stderr, "%s: unable to parse flags: %v %v\n", name, args, err) - os.Exit(1) - } - cfg := getConfig(fs) - if err := setupOperations(cfg, command.Places, command.Simple); err != nil { - fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) - os.Exit(2) - } - - exitCode, err := command.Func(fs) - if err != nil { - fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) - } - if err := cleanupOperations(command.Places); err != nil { - fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) - } - if exitCode != 0 { - os.Exit(exitCode) - } -} - -// Main is the real entrypoint of the zettelstore. -func Main(progName, buildVersion string) { - startup.SetupVersion(progName, buildVersion) - if len(os.Args) <= 1 { - runSimple() - } else { - executeCommand(os.Args[1], os.Args[2:]...) +func parsePort(s string) (string, error) { + port, err := net.LookupPort("tcp", s) + if err != nil { + fmt.Fprintf(os.Stderr, "Wrong port specification: %q", s) + return "", err + } + return strconv.Itoa(port), nil +} + +const ( + keyAdminPort = "admin-port" + keyDefaultDirPlaceType = "default-dir-place-type" + keyInsecureCookie = "insecure-cookie" + keyListenAddr = "listen-addr" + keyOwner = "owner" + keyPersistentCookie = "persistent-cookie" + keyPlaceOneURI = kernel.PlaceURIs + "1" + keyReadOnly = "read-only-mode" + keyTokenLifetimeHTML = "token-lifetime-html" + keyTokenLifetimeAPI = "token-lifetime-api" + keyURLPrefix = "url-prefix" + keyVerbose = "verbose" +) + +func setServiceConfig(cfg *meta.Meta) error { + ok := setConfigValue(true, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose)) + if val, found := cfg.Get(keyAdminPort); found { + ok = setConfigValue(ok, kernel.CoreService, kernel.CorePort, val) + } + + ok = setConfigValue(ok, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, "")) + ok = setConfigValue(ok, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly)) + + ok = setConfigValue( + ok, kernel.PlaceService, kernel.PlaceDefaultDirType, + cfg.GetDefault(keyDefaultDirPlaceType, kernel.PlaceDirTypeNotify)) + ok = setConfigValue(ok, kernel.PlaceService, kernel.PlaceURIs+"1", "dir:./zettel") + format := kernel.PlaceURIs + "%v" + for i := 1; ; i++ { + key := fmt.Sprintf(format, i) + val, found := cfg.Get(key) + if !found { + break + } + ok = setConfigValue(ok, kernel.PlaceService, key, val) + } + + ok = setConfigValue( + ok, kernel.WebService, kernel.WebListenAddress, + cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) + ok = setConfigValue(ok, kernel.WebService, kernel.WebURLPrefix, cfg.GetDefault(keyURLPrefix, "/")) + ok = setConfigValue(ok, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie)) + ok = setConfigValue(ok, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie)) + ok = setConfigValue( + ok, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, "")) + ok = setConfigValue( + ok, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, "")) + + if !ok { + return errors.New("unable to set configuration") + } + return nil +} + +func setConfigValue(ok bool, subsys kernel.Service, key string, val interface{}) bool { + done := kernel.Main.SetConfig(subsys, key, fmt.Sprintf("%v", val)) + if !done { + kernel.Main.Log("unable to set configuration:", key, val) + } + return ok && done +} + +func setupOperations(cfg *meta.Meta, withPlaces bool) { + var createManager kernel.CreatePlaceManagerFunc + if withPlaces { + err := raiseFdLimit() + if err != nil { + srvm := kernel.Main + srvm.Log("Raising some limitions did not work:", err) + srvm.Log("Prepare to encounter errors. Most of them can be mitigated. See the manual for details") + srvm.SetConfig(kernel.PlaceService, kernel.PlaceDefaultDirType, kernel.PlaceDirTypeSimple) + } + createManager = func(placeURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (place.Manager, error) { + progplace.Setup(cfg) + return manager.New(placeURIs, authManager, rtConfig) + } + } else { + createManager = func([]*url.URL, auth.Manager, config.Config) (place.Manager, error) { return nil, nil } + } + + kernel.Main.SetCreators( + func(readonly bool, owner id.Zid) (auth.Manager, error) { + return impl.New(readonly, owner, cfg.GetDefault("secret", "")), nil + }, + createManager, + func(srv server.Server, plMgr place.Manager, authMgr auth.Manager, rtConfig config.Config) error { + setupRouting(srv, plMgr, authMgr, rtConfig) + return nil + }, + ) +} + +func executeCommand(name string, args ...string) int { + command, ok := Get(name) + if !ok { + fmt.Fprintf(os.Stderr, "Unknown command %q\n", name) + return 1 + } + fs := command.GetFlags() + if err := fs.Parse(args); err != nil { + fmt.Fprintf(os.Stderr, "%s: unable to parse flags: %v %v\n", name, args, err) + return 1 + } + cfg := getConfig(fs) + if err := setServiceConfig(cfg); err != nil { + fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) + return 2 + } + setupOperations(cfg, command.Places) + kernel.Main.Start(command.Header) + exitCode, err := command.Func(fs, cfg) + if err != nil { + fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) + } + kernel.Main.Shutdown(true) + return exitCode +} + +// Main is the real entrypoint of the zettelstore. +func Main(progName, buildVersion string) { + kernel.Main.SetConfig(kernel.CoreService, kernel.CoreProgname, progName) + kernel.Main.SetConfig(kernel.CoreService, kernel.CoreVersion, buildVersion) + var exitCode int + if len(os.Args) <= 1 { + exitCode = runSimple() + } else { + exitCode = executeCommand(os.Args[1], os.Args[2:]...) + } + if exitCode != 0 { + os.Exit(exitCode) } } Index: cmd/register.go ================================================================== --- cmd/register.go +++ cmd/register.go @@ -17,15 +17,17 @@ _ "zettelstore.de/z/encoder/jsonenc" // Allow to use JSON encoder. _ "zettelstore.de/z/encoder/nativeenc" // Allow to use native encoder. _ "zettelstore.de/z/encoder/rawenc" // Allow to use raw encoder. _ "zettelstore.de/z/encoder/textenc" // Allow to use text encoder. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. + _ "zettelstore.de/z/kernel/impl" // Allow kernel implementation to create itself _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. _ "zettelstore.de/z/place/constplace" // Allow to use global internal place. _ "zettelstore.de/z/place/dirplace" // Allow to use directory place. _ "zettelstore.de/z/place/fileplace" // Allow to use file place. _ "zettelstore.de/z/place/memplace" // Allow to use memory place. + _ "zettelstore.de/z/place/progplace" // Allow to use computed place. ) Index: collect/split.go ================================================================== --- collect/split.go +++ collect/split.go @@ -12,11 +12,11 @@ package collect import "zettelstore.de/z/ast" // DivideReferences divides the given list of rederences into zettel, local, and external References. -func DivideReferences(all []*ast.Reference, duplicates bool) (zettel, local, external []*ast.Reference) { +func DivideReferences(all []*ast.Reference) (zettel, local, external []*ast.Reference) { if len(all) == 0 { return nil, nil, nil } mapZettel := make(map[string]bool) @@ -25,33 +25,23 @@ for _, ref := range all { if ref.State == ast.RefStateSelf { continue } if ref.IsZettel() { - zettel = appendRefToList(zettel, mapZettel, ref, duplicates) + zettel = appendRefToList(zettel, mapZettel, ref) } else if ref.IsExternal() { - external = appendRefToList(external, mapExternal, ref, duplicates) + external = appendRefToList(external, mapExternal, ref) } else { - local = appendRefToList(local, mapLocal, ref, duplicates) + local = appendRefToList(local, mapLocal, ref) } } return zettel, local, external } -func appendRefToList( - reflist []*ast.Reference, - refSet map[string]bool, - ref *ast.Reference, - duplicates bool, -) []*ast.Reference { - if duplicates { - reflist = append(reflist, ref) - } else { - s := ref.String() - if _, ok := refSet[s]; !ok { - reflist = append(reflist, ref) - refSet[s] = true - } - } - +func appendRefToList(reflist []*ast.Reference, refSet map[string]bool, ref *ast.Reference) []*ast.Reference { + s := ref.String() + if _, ok := refSet[s]; !ok { + reflist = append(reflist, ref) + refSet[s] = true + } return reflist } ADDED config/config.go Index: config/config.go ================================================================== --- config/config.go +++ config/config.go @@ -0,0 +1,108 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package config provides functions to retrieve runtime configuration data. +package config + +import ( + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" +) + +// Config allows to retrieve all defined configuration values that can be changed during runtime. +type Config interface { + AuthConfig + + // AddDefaultValues enriches the given meta data with its default values. + AddDefaultValues(m *meta.Meta) *meta.Meta + + // GetDefaultTitle returns the current value of the "default-title" key. + GetDefaultTitle() string + + // GetDefaultRole returns the current value of the "default-role" key. + GetDefaultRole() string + + // GetDefaultSyntax returns the current value of the "default-syntax" key. + GetDefaultSyntax() string + + // GetDefaultLang returns the current value of the "default-lang" key. + GetDefaultLang() string + + // GetSiteName returns the current value of the "site-name" key. + GetSiteName() string + + // GetHomeZettel returns the value of the "home-zettel" key. + GetHomeZettel() id.Zid + + // GetDefaultVisibility returns the default value for zettel visibility. + GetDefaultVisibility() meta.Visibility + + // GetYAMLHeader returns the current value of the "yaml-header" key. + GetYAMLHeader() bool + + // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. + GetZettelFileSyntax() []string + + // GetMarkerExternal returns the current value of the "marker-external" key. + GetMarkerExternal() string + + // GetFooterHTML returns HTML code that should be embedded into the footer + // of each WebUI page. + GetFooterHTML() string + + // GetListPageSize returns the maximum length of a list to be returned in WebUI. + // A value less or equal to zero signals no limit. + GetListPageSize() int +} + +// AuthConfig are relevant configuration values for authentication. +type AuthConfig interface { + // GetExpertMode returns the current value of the "expert-mode" key + GetExpertMode() bool + + // GetVisibility returns the visibility value of the metadata. + GetVisibility(m *meta.Meta) meta.Visibility +} + +// GetTitle returns the value of the "title" key of the given meta. If there +// is no such value, GetDefaultTitle is returned. +func GetTitle(m *meta.Meta, cfg Config) string { + if val, ok := m.Get(meta.KeyTitle); ok { + return val + } + return cfg.GetDefaultTitle() +} + +// GetRole returns the value of the "role" key of the given meta. If there +// is no such value, GetDefaultRole is returned. +func GetRole(m *meta.Meta, cfg Config) string { + if val, ok := m.Get(meta.KeyRole); ok { + return val + } + return cfg.GetDefaultRole() +} + +// GetSyntax returns the value of the "syntax" key of the given meta. If there +// is no such value, GetDefaultSyntax is returned. +func GetSyntax(m *meta.Meta, cfg Config) string { + if val, ok := m.Get(meta.KeySyntax); ok { + return val + } + return cfg.GetDefaultSyntax() +} + +// GetLang returns the value of the "lang" key of the given meta. If there is +// no such value, GetDefaultLang is returned. +func GetLang(m *meta.Meta, cfg Config) string { + if val, ok := m.Get(meta.KeyLang); ok { + return val + } + return cfg.GetDefaultLang() +} DELETED config/runtime/meta.go Index: config/runtime/meta.go ================================================================== --- config/runtime/meta.go +++ config/runtime/meta.go @@ -1,107 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package runtime provides functions to retrieve runtime configuration data. -package runtime - -import ( - "zettelstore.de/z/config/startup" - "zettelstore.de/z/domain/meta" -) - -var mapDefaultKeys = map[string]func() string{ - meta.KeyCopyright: GetDefaultCopyright, - meta.KeyLang: GetDefaultLang, - meta.KeyLicense: GetDefaultLicense, - meta.KeyRole: GetDefaultRole, - meta.KeySyntax: GetDefaultSyntax, - meta.KeyTitle: GetDefaultTitle, -} - -// AddDefaultValues enriches the given meta data with its default values. -func AddDefaultValues(m *meta.Meta) *meta.Meta { - result := m - for k, f := range mapDefaultKeys { - if _, ok := result.Get(k); !ok { - if result == m { - result = m.Clone() - } - if val := f(); len(val) > 0 || m.Type(k) == meta.TypeEmpty { - result.Set(k, val) - } - } - } - return result -} - -// GetTitle returns the value of the "title" key of the given meta. If there -// is no such value, GetDefaultTitle is returned. -func GetTitle(m *meta.Meta) string { - if syntax, ok := m.Get(meta.KeyTitle); ok && len(syntax) > 0 { - return syntax - } - return GetDefaultTitle() -} - -// GetRole returns the value of the "role" key of the given meta. If there -// is no such value, GetDefaultRole is returned. -func GetRole(m *meta.Meta) string { - if syntax, ok := m.Get(meta.KeyRole); ok && len(syntax) > 0 { - return syntax - } - return GetDefaultRole() -} - -// GetSyntax returns the value of the "syntax" key of the given meta. If there -// is no such value, GetDefaultSyntax is returned. -func GetSyntax(m *meta.Meta) string { - if syntax, ok := m.Get(meta.KeySyntax); ok && len(syntax) > 0 { - return syntax - } - return GetDefaultSyntax() -} - -// GetLang returns the value of the "lang" key of the given meta. If there is -// no such value, GetDefaultLang is returned. -func GetLang(m *meta.Meta) string { - if lang, ok := m.Get(meta.KeyLang); ok && len(lang) > 0 { - return lang - } - return GetDefaultLang() -} - -// GetVisibility returns the visibility value, or "login" if none is given. -func GetVisibility(m *meta.Meta) meta.Visibility { - if val, ok := m.Get(meta.KeyVisibility); ok { - if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown { - return vis - } - } - return GetDefaultVisibility() -} - -// GetUserRole role returns the user role of the given user zettel. -func GetUserRole(user *meta.Meta) meta.UserRole { - if user == nil { - if startup.WithAuth() { - return meta.UserRoleUnknown - } - return meta.UserRoleOwner - } - if startup.IsOwner(user.Zid) { - return meta.UserRoleOwner - } - if val, ok := user.Get(meta.KeyUserRole); ok { - if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { - return ur - } - } - return meta.UserRoleReader -} DELETED config/runtime/runtime.go Index: config/runtime/runtime.go ================================================================== --- config/runtime/runtime.go +++ config/runtime/runtime.go @@ -1,206 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package runtime provides functions to retrieve runtime configuration data. -package runtime - -import ( - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/place" - "zettelstore.de/z/place/stock" -) - -// --- Configuration zettel -------------------------------------------------- - -var configStock stock.Stock - -// SetupConfiguration enables the configuration data. -func SetupConfiguration(mgr place.Manager) { - if configStock != nil { - panic("configStock already set") - } - configStock = stock.NewStock(mgr) - if err := configStock.Subscribe(id.ConfigurationZid); err != nil { - panic(err) - } -} - -// getConfigurationMeta returns the meta data of the configuration zettel. -func getConfigurationMeta() *meta.Meta { - if configStock == nil { - panic("configStock not set") - } - return configStock.GetMeta(id.ConfigurationZid) -} - -// GetDefaultTitle returns the current value of the "default-title" key. -func GetDefaultTitle() string { - if configStock != nil { - if config := getConfigurationMeta(); config != nil { - if title, ok := config.Get(meta.KeyDefaultTitle); ok { - return title - } - } - } - return "Untitled" -} - -// GetDefaultSyntax returns the current value of the "default-syntax" key. -func GetDefaultSyntax() string { - if configStock != nil { - if config := getConfigurationMeta(); config != nil { - if syntax, ok := config.Get(meta.KeyDefaultSyntax); ok { - return syntax - } - } - } - return meta.ValueSyntaxZmk -} - -// GetDefaultRole returns the current value of the "default-role" key. -func GetDefaultRole() string { - if configStock != nil { - if config := getConfigurationMeta(); config != nil { - if role, ok := config.Get(meta.KeyDefaultRole); ok { - return role - } - } - } - return meta.ValueRoleZettel -} - -// GetDefaultLang returns the current value of the "default-lang" key. -func GetDefaultLang() string { - if configStock != nil { - if config := getConfigurationMeta(); config != nil { - if lang, ok := config.Get(meta.KeyDefaultLang); ok { - return lang - } - } - } - return "en" -} - -// GetDefaultCopyright returns the current value of the "default-copyright" key. -func GetDefaultCopyright() string { - if configStock != nil { - if config := getConfigurationMeta(); config != nil { - if copyright, ok := config.Get(meta.KeyDefaultCopyright); ok { - return copyright - } - } - } - return "" -} - -// GetDefaultLicense returns the current value of the "default-license" key. -func GetDefaultLicense() string { - if configStock != nil { - if config := getConfigurationMeta(); config != nil { - if license, ok := config.Get(meta.KeyDefaultLicense); ok { - return license - } - } - } - return "" -} - -// GetExpertMode returns the current value of the "expert-mode" key -func GetExpertMode() bool { - if config := getConfigurationMeta(); config != nil { - if mode, ok := config.Get(meta.KeyExpertMode); ok { - return meta.BoolValue(mode) - } - } - return false -} - -// GetSiteName returns the current value of the "site-name" key. -func GetSiteName() string { - if config := getConfigurationMeta(); config != nil { - if name, ok := config.Get(meta.KeySiteName); ok { - return name - } - } - return "Zettelstore" -} - -// GetHomeZettel returns the value of the "home-zettel" key. -func GetHomeZettel() id.Zid { - if config := getConfigurationMeta(); config != nil { - if start, ok := config.Get(meta.KeyHomeZettel); ok { - if startID, err := id.Parse(start); err == nil { - return startID - } - } - } - return id.DefaultHomeZid -} - -// GetDefaultVisibility returns the default value for zettel visibility. -func GetDefaultVisibility() meta.Visibility { - if config := getConfigurationMeta(); config != nil { - if value, ok := config.Get(meta.KeyDefaultVisibility); ok { - if vis := meta.GetVisibility(value); vis != meta.VisibilityUnknown { - return vis - } - } - } - return meta.VisibilityLogin -} - -// GetYAMLHeader returns the current value of the "yaml-header" key. -func GetYAMLHeader() bool { - if config := getConfigurationMeta(); config != nil { - return config.GetBool(meta.KeyYAMLHeader) - } - return false -} - -// GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. -func GetZettelFileSyntax() []string { - if config := getConfigurationMeta(); config != nil { - return config.GetListOrNil(meta.KeyZettelFileSyntax) - } - return nil -} - -// GetMarkerExternal returns the current value of the "marker-external" key. -func GetMarkerExternal() string { - if config := getConfigurationMeta(); config != nil { - if html, ok := config.Get(meta.KeyMarkerExternal); ok { - return html - } - } - return "➚" -} - -// GetFooterHTML returns HTML code that should be embedded into the footer -// of each WebUI page. -func GetFooterHTML() string { - if config := getConfigurationMeta(); config != nil { - if data, ok := config.Get(meta.KeyFooterHTML); ok { - return data - } - } - return "" -} - -// GetListPageSize returns the maximum length of a list to be returned in WebUI. -// A value less or equal to zero signals no limit. -func GetListPageSize() int { - if config := getConfigurationMeta(); config != nil { - if value, ok := config.GetNumber(meta.KeyListPageSize); ok && value > 0 { - return value - } - } - return 0 -} DELETED config/startup/startup.go Index: config/startup/startup.go ================================================================== --- config/startup/startup.go +++ config/startup/startup.go @@ -1,209 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package startup provides functions to retrieve startup configuration data. -package startup - -import ( - "hash/fnv" - "io" - "strconv" - "time" - - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/index" - "zettelstore.de/z/place" -) - -var config struct { - // Set in SetupStartupConfig - verbose bool - readonlyMode bool - urlPrefix string - listenAddress string - defaultDirPlaceType string - owner id.Zid - withAuth bool - secret []byte - insecCookie bool - persistCookie bool - htmlLifetime time.Duration - apiLifetime time.Duration - - // Set in SetupStartupService - simple bool // was started without run command - manager place.Manager - indexer index.Indexer -} - -// Predefined keys for startup zettel -const ( - KeyDefaultDirPlaceType = "default-dir-place-type" - KeyInsecureCookie = "insecure-cookie" - KeyListenAddress = "listen-addr" - KeyOwner = "owner" - KeyPersistentCookie = "persistent-cookie" - KeyPlaceOneURI = "place-1-uri" - KeyReadOnlyMode = "read-only-mode" - KeyTokenLifetimeHTML = "token-lifetime-html" - KeyTokenLifetimeAPI = "token-lifetime-api" - KeyURLPrefix = "url-prefix" - KeyVerbose = "verbose" -) - -// Important values for some keys. -const ( - ValueDirPlaceTypeNotify = "notify" - ValueDirPlaceTypeSimple = "simple" -) - -// SetupStartupConfig initializes the startup data with content of config file. -func SetupStartupConfig(cfg *meta.Meta) { - if config.urlPrefix != "" { - panic("startup.config already set") - } - config.verbose = cfg.GetBool(KeyVerbose) - config.readonlyMode = cfg.GetBool(KeyReadOnlyMode) - config.urlPrefix = cfg.GetDefault(KeyURLPrefix, "/") - if prefix, ok := cfg.Get(KeyURLPrefix); ok && - len(prefix) > 0 && prefix[0] == '/' && prefix[len(prefix)-1] == '/' { - config.urlPrefix = prefix - } else { - config.urlPrefix = "/" - } - if val, ok := cfg.Get(KeyListenAddress); ok { - config.listenAddress = val // TODO: check for valid string - } else { - config.listenAddress = "127.0.0.1:23123" - } - if defaultType, ok := cfg.Get(KeyDefaultDirPlaceType); ok { - switch defaultType { - case ValueDirPlaceTypeNotify: - case ValueDirPlaceTypeSimple: - default: - defaultType = ValueDirPlaceTypeNotify - } - config.defaultDirPlaceType = defaultType - } else { - config.defaultDirPlaceType = ValueDirPlaceTypeNotify - } - config.owner = id.Invalid - if owner, ok := cfg.Get(KeyOwner); ok { - if zid, err := id.Parse(owner); err == nil { - config.owner = zid - config.withAuth = true - } - } - if config.withAuth { - config.insecCookie = cfg.GetBool(KeyInsecureCookie) - config.persistCookie = cfg.GetBool(KeyPersistentCookie) - config.secret = calcSecret(cfg) - config.htmlLifetime = getDuration( - cfg, KeyTokenLifetimeHTML, 1*time.Hour, 1*time.Minute, 30*24*time.Hour) - config.apiLifetime = getDuration( - cfg, KeyTokenLifetimeAPI, 10*time.Minute, 0, 1*time.Hour) - } -} - -// SetupStartupService initializes the startup data with internal services. -func SetupStartupService(manager place.Manager, idx index.Indexer, simple bool) { - if config.urlPrefix == "" { - panic("startup.config not set") - } - config.simple = simple && !config.withAuth - config.manager = manager - config.indexer = idx -} - -func calcSecret(cfg *meta.Meta) []byte { - h := fnv.New128() - if secret, ok := cfg.Get("secret"); ok { - io.WriteString(h, secret) - } - io.WriteString(h, version.Prog) - io.WriteString(h, version.Build) - io.WriteString(h, version.Hostname) - io.WriteString(h, version.Os) - io.WriteString(h, version.Arch) - return h.Sum(nil) -} - -func getDuration( - cfg *meta.Meta, key string, defDur, minDur, maxDur time.Duration) time.Duration { - if s, ok := cfg.Get(key); ok && len(s) > 0 { - if d, err := strconv.ParseUint(s, 10, 64); err == nil { - secs := time.Duration(d) * time.Minute - if secs < minDur { - return minDur - } - if secs > maxDur { - return maxDur - } - return secs - } - } - return defDur -} - -// IsSimple returns true if Zettelstore was not started with command "run" -// and authentication is disabled. -func IsSimple() bool { return config.simple } - -// IsVerbose returns whether the system should be more chatty about its operations. -func IsVerbose() bool { return config.verbose } - -// IsReadOnlyMode returns whether the system is in read-only mode or not. -func IsReadOnlyMode() bool { return config.readonlyMode } - -// URLPrefix returns the configured prefix to be used when providing URL to -// the service. -func URLPrefix() string { return config.urlPrefix } - -// ListenAddress returns the string that specifies the the network card and the ip port -// where the server listens for requests -func ListenAddress() string { return config.listenAddress } - -// DefaultDirPlaceType returns the default value for a directory place type. -func DefaultDirPlaceType() string { return config.defaultDirPlaceType } - -// WithAuth returns true if user authentication is enabled. -func WithAuth() bool { return config.withAuth } - -// SecureCookie returns whether the web app should set cookies to secure mode. -func SecureCookie() bool { return config.withAuth && !config.insecCookie } - -// PersistentCookie returns whether the web app should set persistent cookies -// (instead of temporary). -func PersistentCookie() bool { return config.persistCookie } - -// Owner returns the zid of the zettelkasten's owner. -// If there is no owner defined, the value ZettelID(0) is returned. -func Owner() id.Zid { return config.owner } - -// IsOwner returns true, if the given user is the owner of the Zettelstore. -func IsOwner(zid id.Zid) bool { return zid.IsValid() && zid == config.owner } - -// Secret returns the interal application secret. It is typically used to -// encrypt session values. -func Secret() []byte { return config.secret } - -// TokenLifetime return the token lifetime for the web/HTML access and for the -// API access. If lifetime for API access is equal to zero, no API access is -// possible. -func TokenLifetime() (htmlLifetime, apiLifetime time.Duration) { - return config.htmlLifetime, config.apiLifetime -} - -// PlaceManager returns the managing place. -func PlaceManager() place.Manager { return config.manager } - -// Indexer returns the current indexer. -func Indexer() index.Indexer { return config.indexer } DELETED config/startup/version.go Index: config/startup/version.go ================================================================== --- config/startup/version.go +++ config/startup/version.go @@ -1,51 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package startup provides functions to retrieve startup configuration data. -package startup - -import ( - "os" - "runtime" -) - -// Version describes all elements of a software version. -type Version struct { - Prog string // Name of the software - Build string // Representation of build process - Hostname string // Host name a reported by the kernel - GoVersion string // Version of go - Os string // GOOS - Arch string // GOARCH - // More to come -} - -var version Version - -// SetupVersion initializes the version data. -func SetupVersion(progName, buildVersion string) { - version.Prog = progName - if buildVersion == "" { - version.Build = "unknown" - } else { - version.Build = buildVersion - } - if hn, err := os.Hostname(); err == nil { - version.Hostname = hn - } else { - version.Hostname = "*unknown host*" - } - version.GoVersion = runtime.Version() - version.Os = runtime.GOOS - version.Arch = runtime.GOARCH -} - -// GetVersion returns the current software version data. -func GetVersion() Version { return version } Index: docs/manual/00001004000000.zettel ================================================================== --- docs/manual/00001004000000.zettel +++ docs/manual/00001004000000.zettel @@ -1,17 +1,21 @@ id: 00001004000000 title: Configuration of Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk - -There are two levels to change the behavior and/or the appearance of Zettelstore. -The first level is the configuration that is needed to start the services provided by Zettelstore. -For example, this includes the URI under which your Zettelstore is accessible. -* [[Zettelstore start-up configuration|00001004010000]] - -The second level is configuring the running Zettelstore. -For example, you can configure the default language of your Zettelstore. -* [[Configure a running Zettelstore|00001004020000]] - -The third level is the way to start Zettelstore services and to manage it. -* [[Command line parameters|00001004050000]] +modified: 20210510153233 + +There are some levels to change the behavior and/or the appearance of Zettelstore. + +# The first level is the way to start Zettelstore services and to manage it via command line (and, in part, via a graphical user interface). +#* [[Command line parameters|00001004050000]] +# As an intermediate user, you usually want to have more control over how Zettelstore is started. + This may include the URI under which your Zettelstore is accessible, or the directories in which your Zettel are stored. + You may want to permanently store the command line parameters so that you don't have to specify them every time you start Zettelstore. +#* [[Zettelstore startup configuration|00001004010000]] +# The last level is configuring the running Zettelstore. + For example, you can configure the default language of your Zettelstore. +#* [[Configure a running Zettelstore|00001004020000]] + +If you have enabled the administrator console, either via [[command-line parameters|00001004050000#a]] or via the [[startup configuration file|00001004010000#admin-port]], you can control the inner workings of Zettelstore even further. +* [[Zettelstore Administrator Console|00001004100000]] Index: docs/manual/00001004010000.zettel ================================================================== --- docs/manual/00001004010000.zettel +++ docs/manual/00001004010000.zettel @@ -1,22 +1,30 @@ id: 00001004010000 -title: Zettelstore start-up configuration +title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk +modified: 20210525121644 -The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some start-up options. +The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options. These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons. For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are placed. An attacker that is able to change the owner can do anything. Therefore only the owner of the computer on which Zettelstore runs can change this information. -The file for start-up configuration must be created via a text editor in advance. +The file for startup configuration must be created via a text editor in advance. The syntax of the configuration file is the same as for any zettel metadata. The following keys are supported: +; [!admin-port]''admin-port'' +: Specifies the TCP port through which you can reach the [[administrator console|00001004100000]]. + A value of ''0'' (the default) disables the administrators console. + + On most operating systems, the value must be greater than ''1024'' unless you start Zettelstore with the full privileges of a system administrator (which is not recommended). + + Default: ''0'' ; [!default-dir-place-type]''default-dir-place-type'' : Specifies the default value for the (sub-) type of [[directory places|00001004011400#type]]. Zettel are typically stored in such places. Default: ''notify'' @@ -42,17 +50,17 @@ If ''true'', a persistent cookie is used. Its lifetime exceeds the lifetime of the authentication token (see option ''token-lifetime-html'') by 30 seconds. Default: ''false'' -; [!place-X-uri]''place-//X//-uri'', where //X// is a number greater or equal to one +; [!place-uri-X]''place-uri-//X//'', where //X// is a number greater or equal to one : Specifies a [[place|00001004011200]] where zettel are stored. - During start-up //X// is counted, starting with one, until no key is found. + During startup //X// is counted up, starting with one, until no key is found. This allows to configure more than one place. - If no ''place-1-uri'' key is given, the overall effect will be the same as if only ''place-1-uri'' was specified with the value ''dir://.zettel''. - In this case, even a key ''place-2-uri'' will be ignored. + If no ''place-uri-1'' key is given, the overall effect will be the same as if only ''place-uri-1'' was specified with the value ''dir://.zettel''. + In this case, even a key ''place-uri-2'' will be ignored. ; [!read-only-mode]''read-only-mode'' : Puts the Zettelstore web service into a read-only mode. No changes are possible. Default: false. ; [!token-lifetime-api]''token-lifetime-api'', [!token-lifetime-html]''token-lifetime-html'' Index: docs/manual/00001004011200.zettel ================================================================== --- docs/manual/00001004011200.zettel +++ docs/manual/00001004011200.zettel @@ -1,10 +1,11 @@ id: 00001004011200 title: Zettelstore places role: manual tags: #configuration #manual #zettelstore syntax: zmk +modified: 20210525121452 A Zettelstore must store its zettel somehow and somewhere. In most cases you want to store your zettel as files in a directory. Under certain circumstances you may want to store your zettel in other places. @@ -11,11 +12,11 @@ An example are the [[predefined zettel|00001005090000]] that come with a Zettelstore. They are stored within the software itself. In another situation you may want to store your zettel volatile, e.g. if you want to provide a sandbox for experimenting. To cope with these (and more) situations, you configure Zettelstore to use one or more places. -This is done via the ''place-X-uri'' keys of the [[start-up configuration|00001004010000#place-X-uri]] (X is a number). +This is done via the ''place-uri-X'' keys of the [[startup configuration|00001004010000#place-uri-X]] (X is a number). Places are specified using special [[URIs|https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]], somehow similar to web addresses. The following place URIs are supported: ; ''dir:\//DIR'' @@ -33,16 +34,13 @@ This place is always read-only. ; ''mem:'' : Stores all its zettel in volatile memory. If you stop the Zettelstore, all changes are lost. -All places that you configure via the ''store-X-uri'' keys form a chain of places. -If a zettel should be retrieved, a search starts in the place specified with the ''place-1-uri'' key, then ''place-2-uri'' and so on. -If a zettel is created or changed, it is always stored in the place specified with the ''place-1-uri'' key. +All places that you configure via the ''store-uri-X'' keys form a chain of places. +If a zettel should be retrieved, a search starts in the place specified with the ''place-uri-2'' key, then ''place-uri-3'' and so on. +If a zettel is created or changed, it is always stored in the place specified with the ''place-uri-1'' key. This allows to overwrite zettel from other places, e.g. the predefined zettel. -If you did not configure the place of the predefined zettel (''const:'') they will automatically be appended as a last place. -Otherwise Zettelstore will not work in certain situations. - -If you use the ''mem:'' place, where zettel are stored in volatile memory, it makes only sense if you configure it as ''place-1-uri''. -Such a place will be empty when Zettelstore starts and only the place 1 will receive updates. +If you use the ''mem:'' place, where zettel are stored in volatile memory, it makes only sense if you configure it as ''place-uri-1''. +Such a place will be empty when Zettelstore starts and only the first place will receive updates. You must make sure that your computer has enough RAM to store all zettel. Index: docs/manual/00001004011400.zettel ================================================================== --- docs/manual/00001004011400.zettel +++ docs/manual/00001004011400.zettel @@ -1,10 +1,11 @@ id: 00001004011400 title: Configure file directory places role: manual tags: #configuration #manual #zettelstore syntax: zmk +modified: 20210525121232 Under certain circumstances, it is preferable to further configure a file directory place. This is done by appending query parameters after the base place URI ''dir:\//DIR''. The following parameters are supported: @@ -38,11 +39,11 @@ Under certain circumstances it is possible that Zettelstore does not detect a change done by another software. To cope with this unlikely, but still possible event, Zettelstore re-scans periodically the file directory. The time interval is configured by the ''rescan'' parameter, e.g. ``` -place-1-uri: dir:///home/zettel?rescan=300 +place-uri-1: dir:///home/zettel?rescan=300 ``` This makes Zettelstore to re-scan the directory ''/home/zettel/'' every 300 seconds, i.e. 5 minutes. For a Zettelstore with many zettel, re-scanning the directory may take a while, especially if it is stored on a remote computer (e.g. via CIFS/SMB or NFS). In this case, you should adjust the parameter value. @@ -74,8 +75,8 @@ === Readonly Sometimes you may want to provide zettel from a file directory place, but you want to disallow any changes. If you provide the query parameter ''readonly'' (with or without a corresponding value), the place will disallow any changes. ``` -place-1-uri: dir:///home/zettel?readonly +place-uri-1: dir:///home/zettel?readonly ``` If you put the whole Zettelstore in [[read-only|00001004010000]] [[mode|00001004051000]], all configured file directory places will be in read-only mode too, even if not explicitly configured. Index: docs/manual/00001004050000.zettel ================================================================== --- docs/manual/00001004050000.zettel +++ docs/manual/00001004050000.zettel @@ -1,10 +1,11 @@ id: 00001004050000 title: Command line parameters role: manual tags: #command #configuration #manual #zettelstore syntax: zmk +modified: 20210511140731 Zettelstore is not just a web service that provides services of a zettelkasten. It allows to some tasks to be executed at the command line. Typically, the task (""sub-command"") will be given at the command line as the first parameter. @@ -19,10 +20,9 @@ ``` Typically this is done by starting Zettelstore via a graphical user interface by double-clicking to its file icon. === Sub-commands * [[``zettelstore help``|00001004050200]] lists all available sub-commands. * [[``zettelstore version``|00001004050400]] to display version information of Zettelstore. -* [[``zettelstore config``|00001004050600]] to show the currently active [[configuration|00001004000000]]. * [[``zettelstore run``|00001004051000]] to start the web-based Zettelstore service. * [[``zettelstore run-simple``|00001004051100]] is typically called, when you start Zettelstore by a double.click in your GUI. * [[``zettelstore file``|00001004051200]] to render files manually without activated/running Zettelstore services. * [[``zettelstore password``|00001004051400]] to calculate data for user authentication. DELETED docs/manual/00001004050600.zettel Index: docs/manual/00001004050600.zettel ================================================================== --- docs/manual/00001004050600.zettel +++ docs/manual/00001004050600.zettel @@ -1,23 +0,0 @@ -id: 00001004050600 -title: The ''config'' sub-command -role: manual -tags: #command #configuration #manual #zettelstore -syntax: zmk -precursor: 00001004050000 - -Shows the Zettelstore configuration, for debugging purposes. -Currently, only the [[start-up configuration|00001004010000]] is shown. - -This sub-command uses the same command line parameters as [[``zettelstore run``|00001004051000]]. - -An example for an unconfigured Zettelstore: -``` -# zettelstore config -Zettelstore (v0.0.4/go1.15) running on mycomputer (linux/amd64) -Stores - Read only = false -Web - Listen Addr = "127.0.0.1:23123" - URL prefix = "/" -``` -The first line is identical to the output of the [[``zettelkasten version``|00001004050400]] sub-command. Index: docs/manual/00001004051000.zettel ================================================================== --- docs/manual/00001004051000.zettel +++ docs/manual/00001004051000.zettel @@ -1,46 +1,49 @@ id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk +modified: 20210510153318 precursor: 00001004050000 === ``zettelstore run`` This starts the web service. ``` zettelstore run [-c CONFIGFILE] [-d DIR] [-p PORT] [-r] [-v] ``` -; ''-c CONFIGFILE'' -: Specifies ''CONFIGFILE'' as a file, where [[start-up configuration data|00001004010000]] is read. +; [!a]''-a PORT'' +: Specifies the TCP port through which you can reach the [[administrator console|00001004100000]]. + See the explanation of [[''admin-port''|00001004010000#admin-port]] for more details. +; [!c]''-c CONFIGFILE'' +: Specifies ''CONFIGFILE'' as a file, where [[startup configuration data|00001004010000]] is read. It is ignored, when the given file is not available, nor readable. Default: ''./.zscfg''. (''.\\.zscfg'' on Windows)), where ''.'' denotes the ""current directory"". -; ''-d DIR'' +; [!d]''-d DIR'' : Specifies ''DIR'' as the directory that contains all zettel. Default is ''./zettel'' (''.\\zettel'' on Windows), where ''.'' denotes the ""current directory"". ; [!debug]''-debug'' : Allows better debugging of the internal web server by disabling any timeout values. You should specify this only as a developer. Especially do not enable it for a production server. [[https://blog.cloudflare.com/exposing-go-on-the-internet/#timeouts]] contains a good explanation for the usefulness of sensitive timeout values. -; ''-p PORT'' -: Specifies the integer value ''PORT'' as the TCP port, where the Zettelstore service listens for requests. +; [!p]''-p PORT'' +: Specifies the integer value ''PORT'' as the TCP port, where the Zettelstore web server listens for requests. Default: 23123. Zettelstore listens only on ''127.0.0.1'', e.g. only requests from the current computer will be processed. - If you want to listen on network card to process requests from other computer, please use ''listen-addr'' of the configuration file as described below. -; ''-r'' + If you want to listen on network card to process requests from other computer, please use [[''listen-addr''|00001004010000#listen-addr]] of the configuration file as described below. +; [!r]''-r'' : Puts the Zettelstore in read-only mode. No changes are possible via the web interface / via the API. This allows to publish your content without any risks of unauthorized changes. -; ''-v'' +; [!v]''-v'' : Be more verbose in writing logs. - Writes the start-up configuration to stderr. -Command line options take precedence over configuration file options. +Command line options take precedence over [[configuration file|00001004010000]] options. ADDED docs/manual/00001004100000.zettel Index: docs/manual/00001004100000.zettel ================================================================== --- docs/manual/00001004100000.zettel +++ docs/manual/00001004100000.zettel @@ -0,0 +1,25 @@ +id: 00001004100000 +title: Zettelstore Administrator Console +role: manual +tags: #configuration #manual #zettelstore +syntax: zmk +modified: 20210510155859 + +The administrator console is a service accessible only on the same computer on which Zettelstore is running. +It allows an experienced user to monitor and control some of the inner workings of Zettelstore. + +You enable the administrator console by specifying a TCP port number greater than zero (better: greater than 1024) for it, either via the [[command-line parameter ''-a''|00001004051000#a]] or via the ''admin-port'' key of the [[startup configuration file|00001004010000#admin-port]]. + +After you enable the administrator console, you can use tools such as [[PuTTY|https://www.chiark.greenend.org.uk/~sgtatham/putty/]] or other telnet software to connect to the administrator console. +In fact, the administrator console is //not// a full telnet service. +It is merely a simple line-oriented service where each input line is interpreted separately. +Therefore, you can also use tools like [[netcat|https://nc110.sourceforge.io/]], [[socat|http://www.dest-unreach.org/socat/]], etc. + +After connecting to the administrator console, there is no further authentication. +It is not needed because you must be logged in on the same computer where Zettelstore is running. +You cannot connect to the administrator console if you are on a different computer. +Of course, on multi-user systems with untrusted users, you should not enable the administrator console. + +* Enable via [[command line|00001004051000#a]] +* Enable via [[configuration file|00001004010000#admin-port]] +* [[List of supported commands|00001004101000]] ADDED docs/manual/00001004101000.zettel Index: docs/manual/00001004101000.zettel ================================================================== --- docs/manual/00001004101000.zettel +++ docs/manual/00001004101000.zettel @@ -0,0 +1,71 @@ +id: 00001004101000 +title: List of supported commands of the administrator console +role: manual +tags: #configuration #manual #zettelstore +syntax: zmk +modified: 20210525161623 + +; ''bye'' +: Closes the connection to the administrator console. +; ''config SERVICE'' +: Displays all valid configuration keys for the given service. + + If a key ends with the hyphen-minus character (""''-''"", ''U+002D''), the key denotes a list value. + Keys of list elements are specified by appending a number greater than zero to the key. +; ''crlf'' +: Toggles CRLF mode for console output. + Changes end of line sequences between Windows mode (==\\r\\n==) and non-Windows mode (==\\n==, initial value). + Often used on Windows telnet clients that otherwise scramble the output of commands. +; ''dump-index'' +: Displays the content of the internal search index. +; ''dump-recover RECOVER'' +: Displays data about the last given recovered internal activity. + + The value for ''RECOVER'' can be obtained via the command ``stat core``, which lists all overview data about all recoveries. +; ''echo'' +: Toggles the echo mode, where each command is printed before execution +; ''env'' +: Display environment values. +; ''help'' +: Displays a list of all available commands. +; ''get-config'' +: Displays current configuration data. + + ``get-config`` shows all current configuration data. + + ``get-config SERVICE`` shows only the current configuration data of the given service. + + ``get-config SERVICE KEY`` shows the current configuration data for the given service and key. +; ''header'' +: Toggles the header mode, where each table is show with a header nor not. +; ''metrics'' +: Displays some values that reflect the inner workings of Zettelstore. + See [[here|https://golang.org/pkg/runtime/metrics/]] for a technical description of these values. +; ''next-config'' +: Displays next configuration data. + It will be the current configuration, if the corresponding services is restarted. + + ``next-config`` shows all next configuration data. + + ``next-config SERVICE`` shows only the next configuration data of the given service. + + ``next-config SERVICE KEY`` shows the next configuration data for the given service and key. +; ''restart SERVICE'' +: Restart the given service and all other that depend on this. +; ''services'' +: Displays s list of all available services and their current status. +; ''set-config SERVICE KEY VALUE'' +: Sets a single configuration value for the next configuration of a given service. + It will become effective if the service is restarted. + + If the key specifies a list value, all other list values with a number greater than the given key are deleted. + You can use the special number ""0"" to delete all values. + E.g. ``set-config place place-uri-0 any_text`` will remove all values of the list //place-uri-//. +; ''shutdown'' +: Terminate the Zettelstore itself (and closes the connection to the administrator console). +; ''start SERVICE'' +: Start the given bservice and all dependent services. +; ''stat SERVICE'' +: Display some statistical values for the given service. +; ''stop SERVICE'' +: Stop the given service and all other that depend on this. Index: docs/manual/00001005000000.zettel ================================================================== --- docs/manual/00001005000000.zettel +++ docs/manual/00001005000000.zettel @@ -20,11 +20,11 @@ === Where zettel are stored Your zettel are stored as files in a specific directory. If you have not explicitly specified the directory, a default directory will be used. -The directory has to be specified at [[start-up time|00001004010000]]. +The directory has to be specified at [[startup time|00001004010000]]. Nested directories are not supported (yet). Every file in this directory that should be monitored by Zettelstore must have a file name that begins with 14 digits (0-9), the [[zettel identifier|00001006050000]]. If you create a new zettel via the web interface or the API, the zettel identifier will be the timestamp of the current date and time (format is ''YYYYMMDDhhmmss''). This allows zettel to be sorted naturally by creation time. Index: docs/manual/00001005090000.zettel ================================================================== --- docs/manual/00001005090000.zettel +++ docs/manual/00001005090000.zettel @@ -1,24 +1,24 @@ id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk +modified: 20210511180816 The following table lists all predefined zettel with their purpose. |= Identifier :|= Title | Purpose | [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore | [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore | [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore -| [[00000000000006]] | Zettelstore Environment Values | Contains environmental data of Zettelstore executable -| [[00000000000008]] | Zettelstore Runtime Values | Contains values that reflect the inner working; see [[here|https://golang.org/pkg/runtime/]] for a technical description of these values -| [[00000000000018]] | Zettelstore Indexer | Provides some statistics about the index process -| [[00000000000020]] | Zettelstore Place Manager | Contains some statistics about zettel places +| [[00000000000004]] | Zettelstore License | Lists the license of Zettelstore +| [[00000000000005]] | Zettelstore Contributors | Lists all contributors of Zettelstore +| [[00000000000006]] | Zettelstore Dependencies | Lists all licensed content +| [[00000000000020]] | Zettelstore Place Manager | Contains some statistics about zettel places and the the index process | [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more -| [[00000000000096]] | Zettelstore Start-up Configuration | Contains the effective values of the [[start-up configuration|00001004010000]] -| [[00000000000098]] | Zettelstore Start-up Values | Contains all values computed from the [[start-up configuration|00001004010000]] +| [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]] | [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]] | [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view | [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]] | [[00000000010300]] | Zettelstore List Meta HTML Template | Used when displaying a list of zettel | [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel @@ -27,13 +27,14 @@ | [[00000000010404]] | Zettelstore Rename Form HTML Template | View that is displayed to change the [[zettel identifier|00001006050000]] | [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel | [[00000000010500]] | Zettelstore List Roles HTML Template | Layout for listing all roles | [[00000000010600]] | Zettelstore List Tags HTML Template | Layout of tags lists | [[00000000020001]] | Zettelstore Base CSS | CSS file that is included by the [[Base HTML Template|00000000010100]] +| [[00000000040001]] | Generic Emoji | Image that is shown if original image reference is invalid | [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu | [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]"" | [[00000000090002]] | New User | Template for a new zettel with role ""[[user|00001006020100#user]]"" | [[00010000000000]] | Home | Default home zettel, contains some welcome information If a zettel is not linked, it is not accessible for the current user. -**Important:** The identifier may change until a stable version of the software is released. +**Important:** All identifier may change until a stable version of the software is released. Index: docs/manual/00001006034500.zettel ================================================================== --- docs/manual/00001006034500.zettel +++ docs/manual/00001006034500.zettel @@ -1,10 +1,11 @@ id: 00001006034500 title: Timestamp Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk +modified: 20210511131903 Values of this type denote a point in time. === Allowed values Must be a sequence of 14 digits (""0""--""9"") (same as an [[Identifier|00001006032000]]), with the restriction that is conforms to the pattern ""YYYYMMDDhhmmss"". @@ -15,13 +16,13 @@ * hh is the hour, * mm is the minute, * ss is the second. === Match operator -A value matches an timestampvalue, if the first value is the prefix of the timestamp value. +A value matches a timestamp value, if the first value is the prefix of the timestamp value. For example, ""202102"" matches ""20210212143200"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. If both values are timestamp values, this works well because both have the same length. Index: docs/manual/00001007031000.zettel ================================================================== --- docs/manual/00001007031000.zettel +++ docs/manual/00001007031000.zettel @@ -1,10 +1,11 @@ id: 00001007031000 title: Zettelmarkup: Tables +role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -role: manual +modified: 20210523185812 Tables are used to show some data in a two-dimenensional fashion. In zettelmarkup, table are not specified explicitly, but by entering //table rows//. Therefore, a table can be seen as a sequence of table rows. A table row is nothing as a sequence of //table cells//. @@ -28,10 +29,11 @@ | a1 | a2 | a3| | b1 | b2 | b3 | c1 | c2 ::: +=== Header row If any cell in the first row of a table contains an equal sing character (""''=''"", ''U+003D'') as the very first character, then this first row will be interpreted as a //table header// row. For example: ```zmk | a1 | a2 |= a3| @@ -43,10 +45,11 @@ | a1 | a2 |= a3| | b1 | b2 | b3 | c1 | c2 ::: +=== Column alignment Inside a header row, you can specify the alignment of each header cell by a given character as the last character of a cell. The alignment of a header cell determines the alignment of every cell in the same column. The following characters specify the alignment: * the colon character (""'':''"", ''U+003A'') forces a centered aligment, @@ -66,10 +69,11 @@ |=Left<|Right>|Center:|Default |123456|123456|123456|123456| |123|123|123|123 ::: +=== Cell alignment To specify the alignment of an individual cell, you can enter these characters for alignment as the first character of that cell. For example: ```zmk |=Left<|Right>|Center:|Default @@ -82,5 +86,22 @@ |=Left<|Right>|Center:|Default |>R|:C|`` tag or the ``