Page MenuHomeMusing Studio

No OneTemporary

diff --git a/config/config.go b/config/config.go
index 5f084a1..7bee863 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1,315 +1,315 @@
/*
* Copyright © 2018-2021 Musing Studio LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/
// Package config holds and assists in the configuration of a writefreely instance.
package config
import (
"net/url"
"strings"
"github.com/go-ini/ini"
"github.com/writeas/web-core/log"
"golang.org/x/net/idna"
)
const (
// FileName is the default configuration file name
FileName = "config.ini"
UserNormal UserType = "user"
UserAdmin = "admin"
)
type (
UserType string
// ServerCfg holds values that affect how the HTTP server runs
ServerCfg struct {
HiddenHost string `ini:"hidden_host"`
Port int `ini:"port"`
Bind string `ini:"bind"`
TLSCertPath string `ini:"tls_cert_path"`
TLSKeyPath string `ini:"tls_key_path"`
Autocert bool `ini:"autocert"`
TemplatesParentDir string `ini:"templates_parent_dir"`
StaticParentDir string `ini:"static_parent_dir"`
PagesParentDir string `ini:"pages_parent_dir"`
KeysParentDir string `ini:"keys_parent_dir"`
HashSeed string `ini:"hash_seed"`
GopherPort int `ini:"gopher_port"`
Dev bool `ini:"-"`
}
// DatabaseCfg holds values that determine how the application connects to a datastore
DatabaseCfg struct {
Type string `ini:"type"`
FileName string `ini:"filename"`
User string `ini:"username"`
Password string `ini:"password"`
Database string `ini:"database"`
Host string `ini:"host"`
Port int `ini:"port"`
TLS bool `ini:"tls"`
}
WriteAsOauthCfg struct {
ClientID string `ini:"client_id"`
ClientSecret string `ini:"client_secret"`
AuthLocation string `ini:"auth_location"`
TokenLocation string `ini:"token_location"`
InspectLocation string `ini:"inspect_location"`
CallbackProxy string `ini:"callback_proxy"`
CallbackProxyAPI string `ini:"callback_proxy_api"`
}
GitlabOauthCfg struct {
ClientID string `ini:"client_id"`
ClientSecret string `ini:"client_secret"`
Host string `ini:"host"`
DisplayName string `ini:"display_name"`
CallbackProxy string `ini:"callback_proxy"`
CallbackProxyAPI string `ini:"callback_proxy_api"`
}
GiteaOauthCfg struct {
ClientID string `ini:"client_id"`
ClientSecret string `ini:"client_secret"`
Host string `ini:"host"`
DisplayName string `ini:"display_name"`
CallbackProxy string `ini:"callback_proxy"`
CallbackProxyAPI string `ini:"callback_proxy_api"`
}
SlackOauthCfg struct {
ClientID string `ini:"client_id"`
ClientSecret string `ini:"client_secret"`
TeamID string `ini:"team_id"`
CallbackProxy string `ini:"callback_proxy"`
CallbackProxyAPI string `ini:"callback_proxy_api"`
}
GenericOauthCfg struct {
ClientID string `ini:"client_id"`
ClientSecret string `ini:"client_secret"`
Host string `ini:"host"`
DisplayName string `ini:"display_name"`
CallbackProxy string `ini:"callback_proxy"`
CallbackProxyAPI string `ini:"callback_proxy_api"`
TokenEndpoint string `ini:"token_endpoint"`
InspectEndpoint string `ini:"inspect_endpoint"`
AuthEndpoint string `ini:"auth_endpoint"`
Scope string `ini:"scope"`
AllowDisconnect bool `ini:"allow_disconnect"`
MapUserID string `ini:"map_user_id"`
MapUsername string `ini:"map_username"`
MapDisplayName string `ini:"map_display_name"`
MapEmail string `ini:"map_email"`
}
// AppCfg holds values that affect how the application functions
AppCfg struct {
SiteName string `ini:"site_name"`
SiteDesc string `ini:"site_description"`
Host string `ini:"host"`
// Site appearance
Theme string `ini:"theme"`
Editor string `ini:"editor"`
JSDisabled bool `ini:"disable_js"`
WebFonts bool `ini:"webfonts"`
Landing string `ini:"landing"`
SimpleNav bool `ini:"simple_nav"`
WFModesty bool `ini:"wf_modesty"`
// Site functionality
Chorus bool `ini:"chorus"`
Forest bool `ini:"forest"` // The admin cares about the forest, not the trees. Hide unnecessary technical info.
DisableDrafts bool `ini:"disable_drafts"`
// Users
SingleUser bool `ini:"single_user"`
OpenRegistration bool `ini:"open_registration"`
OpenDeletion bool `ini:"open_deletion"`
MinUsernameLen int `ini:"min_username_len"`
MaxBlogs int `ini:"max_blogs"`
// Options for public instances
// Federation
Federation bool `ini:"federation"`
PublicStats bool `ini:"public_stats"`
Monetization bool `ini:"monetization"`
NotesOnly bool `ini:"notes_only"`
// Access
Private bool `ini:"private"`
// Additional functions
LocalTimeline bool `ini:"local_timeline"`
UserInvites string `ini:"user_invites"`
// Defaults
DefaultVisibility string `ini:"default_visibility"`
// Check for Updates
UpdateChecks bool `ini:"update_checks"`
// Disable password authentication if use only Oauth
DisablePasswordAuth bool `ini:"disable_password_auth"`
}
EmailCfg struct {
// SMTP configuration values
Host string `ini:"smtp_host"`
Port int `ini:"smtp_port"`
Username string `ini:"smtp_username"`
Password string `ini:"smtp_password"`
EnableStartTLS bool `ini:"smtp_enable_start_tls"`
// Mailgun configuration values
Domain string `ini:"domain"`
MailgunPrivate string `ini:"mailgun_private"`
MailgunEurope bool `ini:"mailgun_europe"`
}
// Config holds the complete configuration for running a writefreely instance
Config struct {
Server ServerCfg `ini:"server"`
Database DatabaseCfg `ini:"database"`
App AppCfg `ini:"app"`
Email EmailCfg `ini:"email"`
SlackOauth SlackOauthCfg `ini:"oauth.slack"`
WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"`
GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"`
GiteaOauth GiteaOauthCfg `ini:"oauth.gitea"`
GenericOauth GenericOauthCfg `ini:"oauth.generic"`
}
)
// New creates a new Config with sane defaults
func New() *Config {
c := &Config{
Server: ServerCfg{
Port: 8080,
Bind: "localhost", /* IPV6 support when not using localhost? */
},
App: AppCfg{
Host: "http://localhost:8080",
Theme: "write",
WebFonts: true,
SingleUser: true,
MinUsernameLen: 3,
MaxBlogs: 1,
Federation: true,
PublicStats: true,
},
}
c.UseMySQL(true)
return c
}
// UseMySQL resets the Config's Database to use default values for a MySQL setup.
func (cfg *Config) UseMySQL(fresh bool) {
cfg.Database.Type = "mysql"
if fresh {
cfg.Database.Host = "localhost"
cfg.Database.Port = 3306
}
}
// UseSQLite resets the Config's Database to use default values for a SQLite setup.
func (cfg *Config) UseSQLite(fresh bool) {
cfg.Database.Type = "sqlite3"
if fresh {
cfg.Database.FileName = "writefreely.db"
}
}
// IsSecureStandalone returns whether or not the application is running as a
// standalone server with TLS enabled.
func (cfg *Config) IsSecureStandalone() bool {
return cfg.Server.Port == 443 && cfg.Server.TLSCertPath != "" && cfg.Server.TLSKeyPath != ""
}
func (ac *AppCfg) LandingPath() string {
if !strings.HasPrefix(ac.Landing, "/") {
return "/" + ac.Landing
}
return ac.Landing
}
func (lc EmailCfg) Enabled() bool {
return (lc.Domain != "" && lc.MailgunPrivate != "") ||
- lc.Username != "" && lc.Password != "" && lc.Host != "" && lc.Port > 0
+ lc.Username != "" && lc.Password != "" && lc.Host != "" && lc.Port > 0
}
func (ac AppCfg) SignupPath() string {
if !ac.OpenRegistration {
return ""
}
if ac.Chorus || ac.Private || (ac.Landing != "" && ac.Landing != "/") {
return "/signup"
}
return "/"
}
// Load reads the given configuration file, then parses and returns it as a Config.
func Load(fname string) (*Config, error) {
if fname == "" {
fname = FileName
}
cfg, err := ini.Load(fname)
if err != nil {
return nil, err
}
// Parse INI file
uc := &Config{}
err = cfg.MapTo(uc)
if err != nil {
return nil, err
}
// Do any transformations
u, err := url.Parse(uc.App.Host)
if err != nil {
return nil, err
}
d, err := idna.ToASCII(u.Hostname())
if err != nil {
log.Error("idna.ToASCII for %s: %s", u.Hostname(), err)
return nil, err
}
uc.App.Host = u.Scheme + "://" + d
if u.Port() != "" {
uc.App.Host += ":" + u.Port()
}
return uc, nil
}
// Save writes the given Config to the given file.
func Save(uc *Config, fname string) error {
cfg := ini.Empty()
err := ini.ReflectFrom(cfg, uc)
if err != nil {
return err
}
if fname == "" {
fname = FileName
}
return cfg.SaveTo(fname)
}
diff --git a/mailer/mailer.go b/mailer/mailer.go
index ea0c7f9..30892e6 100644
--- a/mailer/mailer.go
+++ b/mailer/mailer.go
@@ -1,181 +1,181 @@
/*
* Copyright © 2024 Musing Studio LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/
package mailer
import (
"fmt"
- "strings"
"github.com/mailgun/mailgun-go"
- "github.com/writefreely/writefreely/config"
"github.com/writeas/web-core/log"
+ "github.com/writefreely/writefreely/config"
mail "github.com/xhit/go-simple-mail/v2"
+ "strings"
)
type (
// Mailer holds configurations for the preferred mailing provider.
Mailer struct {
smtp *mail.SMTPServer
mailGun *mailgun.MailgunImpl
}
// Message holds the email contents and metadata for the preferred mailing provider.
Message struct {
mgMsg *mailgun.Message
smtpMsg *SmtpMessage
}
SmtpMessage struct {
- from string
- replyTo string
- subject string
+ from string
+ replyTo string
+ subject string
recipients []Recipient
- html string
- text string
+ html string
+ text string
}
Recipient struct {
email string
- vars map[string]string
+ vars map[string]string
}
)
// New creates a new Mailer from the instance's config.EmailCfg, returning an error if not properly configured.
func New(eCfg config.EmailCfg) (*Mailer, error) {
m := &Mailer{}
if eCfg.Domain != "" && eCfg.MailgunPrivate != "" {
m.mailGun = mailgun.NewMailgun(eCfg.Domain, eCfg.MailgunPrivate)
if eCfg.MailgunEurope {
m.mailGun.SetAPIBase("https://api.eu.mailgun.net/v3")
}
} else if eCfg.Username != "" && eCfg.Password != "" && eCfg.Host != "" && eCfg.Port > 0 {
m.smtp = mail.NewSMTPClient()
m.smtp.Host = eCfg.Host
m.smtp.Port = eCfg.Port
m.smtp.Username = eCfg.Username
m.smtp.Password = eCfg.Password
if eCfg.EnableStartTLS {
m.smtp.Encryption = mail.EncryptionSTARTTLS
}
// To allow sending multiple email
m.smtp.KeepAlive = true
} else {
return nil, fmt.Errorf("no email provider is configured")
}
return m, nil
}
// NewMessage creates a new Message from the given parameters.
func (m *Mailer) NewMessage(from, subject, text string, to ...string) (*Message, error) {
msg := &Message{}
if m.mailGun != nil {
msg.mgMsg = m.mailGun.NewMessage(from, subject, text, to...)
} else if m.smtp != nil {
- msg.smtpMsg = &SmtpMessage {
- from,
- "",
- subject,
- make([]Recipient, len(to)),
- "",
- text,
+ msg.smtpMsg = &SmtpMessage{
+ from: from,
+ replyTo: "",
+ subject: subject,
+ recipients: make([]Recipient, len(to)),
+ html: "",
+ text: text,
}
for _, r := range to {
msg.smtpMsg.recipients = append(msg.smtpMsg.recipients, Recipient{r, make(map[string]string)})
}
}
return msg, nil
}
// SetHTML sets the body of the message.
func (m *Message) SetHTML(html string) {
if m.smtpMsg != nil {
m.smtpMsg.html = html
} else if m.mgMsg != nil {
m.mgMsg.SetHtml(html)
}
}
func (m *Message) SetReplyTo(replyTo string) {
- if (m.smtpMsg != nil) {
+ if m.smtpMsg != nil {
m.smtpMsg.replyTo = replyTo
} else {
m.mgMsg.SetReplyTo(replyTo)
}
}
// AddTag attaches a tag to the Message for providers that support it.
func (m *Message) AddTag(tag string) {
if m.mgMsg != nil {
m.mgMsg.AddTag(tag)
}
}
func (m *Message) AddRecipientAndVariables(r string, vars map[string]string) error {
if m.smtpMsg != nil {
m.smtpMsg.recipients = append(m.smtpMsg.recipients, Recipient{r, vars})
return nil
} else {
varsInterfaces := make(map[string]interface{}, len(vars))
for k, v := range vars {
varsInterfaces[k] = v
}
return m.mgMsg.AddRecipientAndVariables(r, varsInterfaces)
}
}
// Send sends the given message via the preferred provider.
func (m *Mailer) Send(msg *Message) error {
if m.smtp != nil {
client, err := m.smtp.Connect()
if err != nil {
return err
}
emailSent := false
for _, r := range msg.smtpMsg.recipients {
customMsg := mail.NewMSG()
customMsg.SetFrom(msg.smtpMsg.from)
- if (msg.smtpMsg.replyTo != "") {
+ if msg.smtpMsg.replyTo != "" {
customMsg.SetReplyTo(msg.smtpMsg.replyTo)
}
customMsg.SetSubject(msg.smtpMsg.subject)
customMsg.AddTo(r.email)
cText := msg.smtpMsg.text
cHtml := msg.smtpMsg.html
for v, value := range r.vars {
placeHolder := fmt.Sprintf("%%recipient.%s%%", v)
cText = strings.ReplaceAll(cText, placeHolder, value)
cHtml = strings.ReplaceAll(cHtml, placeHolder, value)
}
customMsg.SetBody(mail.TextHTML, cHtml)
customMsg.AddAlternative(mail.TextPlain, cText)
e := customMsg.Error
if e == nil {
e = customMsg.Send(client)
}
if e == nil {
emailSent = true
} else {
- log.Error("Unable to send email to %s: %v", r.email, e)
+ log.Error("Unable to send email to %s: %v", r.email, e)
err = e
}
}
if !emailSent {
// only send an error if no email could be sent (to avoid retry of successfully sent emails)
return err
}
} else if m.mailGun != nil {
_, _, err := m.mailGun.Send(msg.mgMsg)
if err != nil {
return err
}
}
return nil
}
diff --git a/templates.go b/templates.go
index 3bb7d13..030467c 100644
--- a/templates.go
+++ b/templates.go
@@ -1,240 +1,240 @@
/*
* Copyright © 2018-2021 Musing Studio LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/
package writefreely
import (
"errors"
"html/template"
"io"
- "os"
"net/http"
+ "os"
"path/filepath"
"strings"
"github.com/dustin/go-humanize"
"github.com/writeas/web-core/l10n"
"github.com/writeas/web-core/log"
"github.com/writefreely/writefreely/config"
)
var (
templates = map[string]*template.Template{}
pages = map[string]*template.Template{}
userPages = map[string]*template.Template{}
funcMap = template.FuncMap{
"largeNumFmt": largeNumFmt,
"pluralize": pluralize,
"isRTL": isRTL,
"isLTR": isLTR,
"localstr": localStr,
"localhtml": localHTML,
"tolower": strings.ToLower,
"title": strings.Title,
"hasPrefix": strings.HasPrefix,
"hasSuffix": strings.HasSuffix,
"dict": dict,
}
)
const (
templatesDir = "templates"
pagesDir = "pages"
)
func showUserPage(w http.ResponseWriter, name string, obj interface{}) {
if obj == nil {
log.Error("showUserPage: data is nil!")
return
}
if err := userPages[filepath.Join("user", name+".tmpl")].ExecuteTemplate(w, name, obj); err != nil {
log.Error("Error parsing %s: %v", name, err)
}
}
func initTemplate(parentDir, name string) {
if debugging {
log.Info(" " + filepath.Join(parentDir, templatesDir, name+".tmpl"))
}
files := []string{
filepath.Join(parentDir, templatesDir, name+".tmpl"),
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
filepath.Join(parentDir, templatesDir, "base.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"),
}
if name == "collection" || name == "collection-tags" || name == "chorus-collection" || name == "read" {
// These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl"
files = append(files, filepath.Join(parentDir, templatesDir, "include", "posts.tmpl"))
}
if name == "chorus-collection" || name == "chorus-collection-post" {
files = append(files, filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"))
}
if name == "collection" || name == "collection-tags" || name == "collection-post" || name == "post" || name == "chorus-collection" || name == "chorus-collection-post" {
files = append(files, filepath.Join(parentDir, templatesDir, "include", "post-render.tmpl"))
}
templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...))
}
func initPage(parentDir, path, key string) {
if debugging {
log.Info(" [%s] %s", key, path)
}
files := []string{
path,
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
filepath.Join(parentDir, templatesDir, "base.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"),
}
if key == "login.tmpl" || key == "landing.tmpl" || key == "signup.tmpl" {
files = append(files, filepath.Join(parentDir, templatesDir, "include", "oauth.tmpl"))
}
pages[key] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...))
}
func initUserPage(parentDir, path, key string) {
if debugging {
log.Info(" [%s] %s", key, path)
}
userPages[key] = template.Must(template.New(key).Funcs(funcMap).ParseFiles(
path,
filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "nav.tmpl"),
))
}
// InitTemplates loads all template files from the configured parent dir.
func InitTemplates(cfg *config.Config) error {
log.Info("Loading templates...")
tmplFiles, err := os.ReadDir(filepath.Join(cfg.Server.TemplatesParentDir, templatesDir))
if err != nil {
return err
}
for _, f := range tmplFiles {
if !f.IsDir() && !strings.HasPrefix(f.Name(), ".") {
parts := strings.Split(f.Name(), ".")
key := parts[0]
initTemplate(cfg.Server.TemplatesParentDir, key)
}
}
log.Info("Loading pages...")
// Initialize all static pages that use the base template
err = filepath.Walk(filepath.Join(cfg.Server.PagesParentDir, pagesDir), func(path string, i os.FileInfo, err error) error {
if err != nil {
return err
}
if !i.IsDir() && !strings.HasPrefix(i.Name(), ".") {
key := i.Name()
initPage(cfg.Server.PagesParentDir, path, key)
}
return nil
})
if err != nil {
return err
}
log.Info("Loading user pages...")
// Initialize all user pages that use base templates
err = filepath.Walk(filepath.Join(cfg.Server.TemplatesParentDir, templatesDir, "user"), func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if !f.IsDir() && !strings.HasPrefix(f.Name(), ".") {
corePath := path
if cfg.Server.TemplatesParentDir != "" {
corePath = corePath[len(cfg.Server.TemplatesParentDir)+1:]
}
parts := strings.Split(corePath, string(filepath.Separator))
key := f.Name()
if len(parts) > 2 {
key = filepath.Join(parts[1], f.Name())
}
initUserPage(cfg.Server.TemplatesParentDir, path, key)
}
return nil
})
if err != nil {
return err
}
return nil
}
// renderPage retrieves the given template and renders it to the given io.Writer.
// If something goes wrong, the error is logged and returned.
func renderPage(w io.Writer, tmpl string, data interface{}) error {
err := pages[tmpl].ExecuteTemplate(w, "base", data)
if err != nil {
log.Error("%v", err)
}
return err
}
func largeNumFmt(n int64) string {
return humanize.Comma(n)
}
func pluralize(singular, plural string, n int64) string {
if n == 1 {
return singular
}
return plural
}
func isRTL(d string) bool {
return d == "rtl"
}
func isLTR(d string) bool {
return d == "ltr" || d == "auto"
}
func localStr(term, lang string) string {
s := l10n.Strings(lang)[term]
if s == "" {
s = l10n.Strings("")[term]
}
return s
}
func localHTML(term, lang string) template.HTML {
s := l10n.Strings(lang)[term]
if s == "" {
s = l10n.Strings("")[term]
}
s = strings.Replace(s, "write.as", "<a href=\"https://writefreely.org\">writefreely</a>", 1)
return template.HTML(s)
}
// from: https://stackoverflow.com/a/18276968/1549194
func dict(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("dict: invalid number of parameters")
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict: keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, May 16, 4:00 AM (1 d, 11 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3239969

Event Timeline