~ttt/minifluxlite

228862fefaa645026caa483ffe9993bf8c00b22e — Frédéric Guillot 4 years ago 04d85b3
Refactor config package

- Parse configuration only once during startup time
- Store configuration values in a global variable
M cli/cli.go => cli/cli.go +16 -6
@@ 51,9 51,11 @@ func Parse() {
	flag.BoolVar(&flagDebugMode, "debug", false, flagDebugModeHelp)
	flag.Parse()

	cfg := config.NewConfig()
	if err := config.ParseConfig(); err != nil {
		logger.Fatal("%v", err)
	}

	if flagDebugMode || cfg.HasDebugMode() {
	if flagDebugMode || config.Opts.HasDebugMode() {
		logger.EnableDebug()
	}



@@ 67,7 69,15 @@ func Parse() {
		return
	}

	db, err := database.NewConnectionPool(cfg.DatabaseURL(), cfg.DatabaseMinConns(), cfg.DatabaseMaxConns())
	if config.Opts.IsDefaultDatabaseURL() {
		logger.Info("The default value for DATABASE_URL is used")
	}

	db, err := database.NewConnectionPool(
		config.Opts.DatabaseURL(),
		config.Opts.DatabaseMinConns(),
		config.Opts.DatabaseMaxConns(),
	)
	if err != nil {
		logger.Fatal("Unable to connect to the database: %v", err)
	}


@@ 101,14 111,14 @@ func Parse() {
	}

	// Run migrations and start the deamon.
	if cfg.RunMigrations() {
	if config.Opts.RunMigrations() {
		database.Migrate(db)
	}

	// Create admin user and start the deamon.
	if cfg.CreateAdmin() {
	if config.Opts.CreateAdmin() {
		createAdmin(store)
	}

	startDaemon(cfg, store)
	startDaemon(store)
}

M cli/daemon.go => cli/daemon.go +8 -8
@@ 16,13 16,13 @@ import (
	"miniflux.app/config"
	"miniflux.app/logger"
	"miniflux.app/reader/feed"
	"miniflux.app/service/scheduler"
	"miniflux.app/service/httpd"
	"miniflux.app/service/scheduler"
	"miniflux.app/storage"
	"miniflux.app/worker"
)

func startDaemon(cfg *config.Config, store *storage.Storage) {
func startDaemon(store *storage.Storage) {
	logger.Info("Starting Miniflux...")

	stop := make(chan os.Signal, 1)


@@ 30,17 30,17 @@ func startDaemon(cfg *config.Config, store *storage.Storage) {
	signal.Notify(stop, syscall.SIGTERM)

	feedHandler := feed.NewFeedHandler(store)
	pool := worker.NewPool(feedHandler, cfg.WorkerPoolSize())
	pool := worker.NewPool(feedHandler, config.Opts.WorkerPoolSize())

	go showProcessStatistics()

	if cfg.HasSchedulerService() {
		scheduler.Serve(cfg, store, pool)
	if config.Opts.HasSchedulerService() {
		scheduler.Serve(store, pool)
	}

	var httpServer *http.Server
	if cfg.HasHTTPService() {
		httpServer = httpd.Serve(cfg, store, pool, feedHandler)
	if config.Opts.HasHTTPService() {
		httpServer = httpd.Serve(store, pool, feedHandler)
	}

	<-stop


@@ 64,4 64,4 @@ func showProcessStatistics() {
			runtime.NumGoroutine(), runtime.NumCPU())
		time.Sleep(30 * time.Second)
	}
}
\ No newline at end of file
}

M config/config.go => config/config.go +6 -266
@@ 4,271 4,11 @@

package config // import "miniflux.app/config"

import (
	"net/url"
	"os"
	"strconv"
	"strings"
// Opts contains configuration options after parsing.
var Opts *Options

	"miniflux.app/logger"
)

const (
	defaultBaseURL            = "http://localhost"
	defaultDatabaseURL        = "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
	defaultWorkerPoolSize     = 5
	defaultPollingFrequency   = 60
	defaultBatchSize          = 10
	defaultDatabaseMaxConns   = 20
	defaultDatabaseMinConns   = 1
	defaultArchiveReadDays    = 60
	defaultListenAddr         = "127.0.0.1:8080"
	defaultCertFile           = ""
	defaultKeyFile            = ""
	defaultCertDomain         = ""
	defaultCertCache          = "/tmp/cert_cache"
	defaultCleanupFrequency   = 24
	defaultProxyImages        = "http-only"
	defaultOAuth2ClientID     = ""
	defaultOAuth2ClientSecret = ""
	defaultOAuth2RedirectURL  = ""
	defaultOAuth2Provider     = ""
)

// Config manages configuration parameters.
type Config struct {
	IsHTTPS  bool
	baseURL  string
	rootURL  string
	basePath string
}

func (c *Config) parseBaseURL() {
	baseURL := os.Getenv("BASE_URL")
	if baseURL == "" {
		return
	}

	if baseURL[len(baseURL)-1:] == "/" {
		baseURL = baseURL[:len(baseURL)-1]
	}

	u, err := url.Parse(baseURL)
	if err != nil {
		logger.Error("Invalid BASE_URL: %v", err)
		return
	}

	scheme := strings.ToLower(u.Scheme)
	if scheme != "https" && scheme != "http" {
		logger.Error("Invalid BASE_URL: scheme must be http or https")
		return
	}

	c.baseURL = baseURL
	c.basePath = u.Path

	u.Path = ""
	c.rootURL = u.String()
}

// HasDebugMode returns true if debug mode is enabled.
func (c *Config) HasDebugMode() bool {
	return getBooleanValue("DEBUG")
}

// BaseURL returns the application base URL with path.
func (c *Config) BaseURL() string {
	return c.baseURL
}

// RootURL returns the base URL without path.
func (c *Config) RootURL() string {
	return c.rootURL
}

// BasePath returns the application base path according to the base URL.
func (c *Config) BasePath() string {
	return c.basePath
}

// DatabaseURL returns the database URL.
func (c *Config) DatabaseURL() string {
	value, exists := os.LookupEnv("DATABASE_URL")
	if !exists {
		logger.Info("The environment variable DATABASE_URL is not configured (the default value is used instead)")
	}

	if value == "" {
		value = defaultDatabaseURL
	}

	return value
}

// DatabaseMaxConns returns the maximum number of database connections.
func (c *Config) DatabaseMaxConns() int {
	return getIntValue("DATABASE_MAX_CONNS", defaultDatabaseMaxConns)
}

// DatabaseMinConns returns the minimum number of database connections.
func (c *Config) DatabaseMinConns() int {
	return getIntValue("DATABASE_MIN_CONNS", defaultDatabaseMinConns)
}

// ListenAddr returns the listen address for the HTTP server.
func (c *Config) ListenAddr() string {
	if port := os.Getenv("PORT"); port != "" {
		return ":" + port
	}

	return getStringValue("LISTEN_ADDR", defaultListenAddr)
}

// CertFile returns the SSL certificate filename if any.
func (c *Config) CertFile() string {
	return getStringValue("CERT_FILE", defaultCertFile)
}

// KeyFile returns the private key filename for custom SSL certificate.
func (c *Config) KeyFile() string {
	return getStringValue("KEY_FILE", defaultKeyFile)
}

// CertDomain returns the domain to use for Let's Encrypt certificate.
func (c *Config) CertDomain() string {
	return getStringValue("CERT_DOMAIN", defaultCertDomain)
}

// CertCache returns the directory to use for Let's Encrypt session cache.
func (c *Config) CertCache() string {
	return getStringValue("CERT_CACHE", defaultCertCache)
}

// CleanupFrequency returns the interval for cleanup jobs.
func (c *Config) CleanupFrequency() int {
	return getIntValue("CLEANUP_FREQUENCY", defaultCleanupFrequency)
}

// WorkerPoolSize returns the number of background worker.
func (c *Config) WorkerPoolSize() int {
	return getIntValue("WORKER_POOL_SIZE", defaultWorkerPoolSize)
}

// PollingFrequency returns the interval to refresh feeds in the background.
func (c *Config) PollingFrequency() int {
	return getIntValue("POLLING_FREQUENCY", defaultPollingFrequency)
}

// BatchSize returns the number of feeds to send for background processing.
func (c *Config) BatchSize() int {
	return getIntValue("BATCH_SIZE", defaultBatchSize)
}

// IsOAuth2UserCreationAllowed returns true if user creation is allowed for OAuth2 users.
func (c *Config) IsOAuth2UserCreationAllowed() bool {
	return getBooleanValue("OAUTH2_USER_CREATION")
}

// OAuth2ClientID returns the OAuth2 Client ID.
func (c *Config) OAuth2ClientID() string {
	return getStringValue("OAUTH2_CLIENT_ID", defaultOAuth2ClientID)
}

// OAuth2ClientSecret returns the OAuth2 client secret.
func (c *Config) OAuth2ClientSecret() string {
	return getStringValue("OAUTH2_CLIENT_SECRET", defaultOAuth2ClientSecret)
}

// OAuth2RedirectURL returns the OAuth2 redirect URL.
func (c *Config) OAuth2RedirectURL() string {
	return getStringValue("OAUTH2_REDIRECT_URL", defaultOAuth2RedirectURL)
}

// OAuth2Provider returns the name of the OAuth2 provider configured.
func (c *Config) OAuth2Provider() string {
	return getStringValue("OAUTH2_PROVIDER", defaultOAuth2Provider)
}

// HasHSTS returns true if HTTP Strict Transport Security is enabled.
func (c *Config) HasHSTS() bool {
	return !getBooleanValue("DISABLE_HSTS")
}

// RunMigrations returns true if the environment variable RUN_MIGRATIONS is not empty.
func (c *Config) RunMigrations() bool {
	return getBooleanValue("RUN_MIGRATIONS")
}

// CreateAdmin returns true if the environment variable CREATE_ADMIN is not empty.
func (c *Config) CreateAdmin() bool {
	return getBooleanValue("CREATE_ADMIN")
}

// PocketConsumerKey returns the Pocket Consumer Key if defined as environment variable.
func (c *Config) PocketConsumerKey(defaultValue string) string {
	return getStringValue("POCKET_CONSUMER_KEY", defaultValue)
}

// ProxyImages returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
func (c *Config) ProxyImages() string {
	return getStringValue("PROXY_IMAGES", defaultProxyImages)
}

// HasHTTPService returns true if the HTTP service is enabled.
func (c *Config) HasHTTPService() bool {
	return !getBooleanValue("DISABLE_HTTP_SERVICE")
}

// HasSchedulerService returns true if the scheduler service is enabled.
func (c *Config) HasSchedulerService() bool {
	return !getBooleanValue("DISABLE_SCHEDULER_SERVICE")
}

// ArchiveReadDays returns the number of days after which marking read items as removed.
func (c *Config) ArchiveReadDays() int {
	return getIntValue("ARCHIVE_READ_DAYS", defaultArchiveReadDays)
}

// NewConfig returns a new Config.
func NewConfig() *Config {
	cfg := &Config{
		baseURL: defaultBaseURL,
		rootURL: defaultBaseURL,
		IsHTTPS: getBooleanValue("HTTPS"),
	}

	cfg.parseBaseURL()
	return cfg
}

func getBooleanValue(key string) bool {
	value := strings.ToLower(os.Getenv(key))
	if value == "1" || value == "yes" || value == "true" || value == "on" {
		return true
	}
	return false
}

func getStringValue(key, fallback string) string {
	value := os.Getenv(key)
	if value == "" {
		return fallback
	}

	return value
}

func getIntValue(key string, fallback int) int {
	value := os.Getenv(key)
	if value == "" {
		return fallback
	}

	v, err := strconv.Atoi(value)
	if err != nil {
		return fallback
	}

	return v
// ParseConfig parses configuration options.
func ParseConfig() (err error) {
	Opts, err = parse()
	return err
}

M config/config_test.go => config/config_test.go +382 -236
@@ 1,4 1,4 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Copyright 2019 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.



@@ 9,198 9,125 @@ import (
	"testing"
)

func TestGetBooleanValueWithUnsetVariable(t *testing.T) {
	os.Clearenv()
	if getBooleanValue("MY_TEST_VARIABLE") {
		t.Errorf(`Unset variables should returns false`)
	}
}

func TestGetBooleanValue(t *testing.T) {
	scenarios := map[string]bool{
		"":        false,
		"1":       true,
		"Yes":     true,
		"yes":     true,
		"True":    true,
		"true":    true,
		"on":      true,
		"false":   false,
		"off":     false,
		"invalid": false,
	}

	for input, expected := range scenarios {
		os.Clearenv()
		os.Setenv("MY_TEST_VARIABLE", input)
		result := getBooleanValue("MY_TEST_VARIABLE")
		if result != expected {
			t.Errorf(`Unexpected result for %q, got %v instead of %v`, input, result, expected)
		}
	}
}

func TestGetStringValueWithUnsetVariable(t *testing.T) {
	os.Clearenv()
	if getStringValue("MY_TEST_VARIABLE", "defaultValue") != "defaultValue" {
		t.Errorf(`Unset variables should returns the default value`)
	}
}

func TestGetStringValue(t *testing.T) {
func TestDebugModeOn(t *testing.T) {
	os.Clearenv()
	os.Setenv("MY_TEST_VARIABLE", "test")
	if getStringValue("MY_TEST_VARIABLE", "defaultValue") != "test" {
		t.Errorf(`Defined variables should returns the specified value`)
	}
}
	os.Setenv("DEBUG", "1")

func TestGetIntValueWithUnsetVariable(t *testing.T) {
	os.Clearenv()
	if getIntValue("MY_TEST_VARIABLE", 42) != 42 {
		t.Errorf(`Unset variables should returns the default value`)
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}
}

func TestGetIntValueWithInvalidInput(t *testing.T) {
	os.Clearenv()
	os.Setenv("MY_TEST_VARIABLE", "invalid integer")
	if getIntValue("MY_TEST_VARIABLE", 42) != 42 {
		t.Errorf(`Invalid integer should returns the default value`)
	if !opts.HasDebugMode() {
		t.Fatalf(`Unexpected debug mode value, got "%v"`, opts.HasDebugMode())
	}
}

func TestGetIntValue(t *testing.T) {
	os.Clearenv()
	os.Setenv("MY_TEST_VARIABLE", "2018")
	if getIntValue("MY_TEST_VARIABLE", 42) != 2018 {
		t.Errorf(`Defined variables should returns the specified value`)
	}
}

func TestDebugModeOn(t *testing.T) {
func TestDebugModeOff(t *testing.T) {
	os.Clearenv()
	os.Setenv("DEBUG", "1")
	cfg := NewConfig()

	if !cfg.HasDebugMode() {
		t.Fatalf(`Unexpected debug mode value, got "%v"`, cfg.HasDebugMode())
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}
}

func TestDebugModeOff(t *testing.T) {
	os.Clearenv()
	cfg := NewConfig()

	if cfg.HasDebugMode() {
		t.Fatalf(`Unexpected debug mode value, got "%v"`, cfg.HasDebugMode())
	if opts.HasDebugMode() {
		t.Fatalf(`Unexpected debug mode value, got "%v"`, opts.HasDebugMode())
	}
}

func TestCustomBaseURL(t *testing.T) {
	os.Clearenv()
	os.Setenv("BASE_URL", "http://example.org")
	cfg := NewConfig()

	if cfg.BaseURL() != "http://example.org" {
		t.Fatalf(`Unexpected base URL, got "%s"`, cfg.BaseURL())
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	if cfg.RootURL() != "http://example.org" {
		t.Fatalf(`Unexpected root URL, got "%s"`, cfg.RootURL())
	if opts.BaseURL() != "http://example.org" {
		t.Fatalf(`Unexpected base URL, got "%s"`, opts.BaseURL())
	}

	if cfg.BasePath() != "" {
		t.Fatalf(`Unexpected base path, got "%s"`, cfg.BasePath())
	if opts.RootURL() != "http://example.org" {
		t.Fatalf(`Unexpected root URL, got "%s"`, opts.RootURL())
	}

	if opts.BasePath() != "" {
		t.Fatalf(`Unexpected base path, got "%s"`, opts.BasePath())
	}
}

func TestCustomBaseURLWithTrailingSlash(t *testing.T) {
	os.Clearenv()
	os.Setenv("BASE_URL", "http://example.org/folder/")
	cfg := NewConfig()

	if cfg.BaseURL() != "http://example.org/folder" {
		t.Fatalf(`Unexpected base URL, got "%s"`, cfg.BaseURL())
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	if cfg.RootURL() != "http://example.org" {
		t.Fatalf(`Unexpected root URL, got "%s"`, cfg.RootURL())
	if opts.BaseURL() != "http://example.org/folder" {
		t.Fatalf(`Unexpected base URL, got "%s"`, opts.BaseURL())
	}

	if cfg.BasePath() != "/folder" {
		t.Fatalf(`Unexpected base path, got "%s"`, cfg.BasePath())
	if opts.RootURL() != "http://example.org" {
		t.Fatalf(`Unexpected root URL, got "%s"`, opts.RootURL())
	}

	if opts.BasePath() != "/folder" {
		t.Fatalf(`Unexpected base path, got "%s"`, opts.BasePath())
	}
}

func TestBaseURLWithoutScheme(t *testing.T) {
	os.Clearenv()
	os.Setenv("BASE_URL", "example.org/folder/")
	cfg := NewConfig()

	if cfg.BaseURL() != "http://localhost" {
		t.Fatalf(`Unexpected base URL, got "%s"`, cfg.BaseURL())
	}

	if cfg.RootURL() != "http://localhost" {
		t.Fatalf(`Unexpected root URL, got "%s"`, cfg.RootURL())
	}

	if cfg.BasePath() != "" {
		t.Fatalf(`Unexpected base path, got "%s"`, cfg.BasePath())
	_, err := parse()
	if err == nil {
		t.Fatalf(`Parsing must fail`)
	}
}

func TestBaseURLWithInvalidScheme(t *testing.T) {
	os.Clearenv()
	os.Setenv("BASE_URL", "ftp://example.org/folder/")
	cfg := NewConfig()

	if cfg.BaseURL() != "http://localhost" {
		t.Fatalf(`Unexpected base URL, got "%s"`, cfg.BaseURL())
	}

	if cfg.RootURL() != "http://localhost" {
		t.Fatalf(`Unexpected root URL, got "%s"`, cfg.RootURL())
	}

	if cfg.BasePath() != "" {
		t.Fatalf(`Unexpected base path, got "%s"`, cfg.BasePath())
	_, err := parse()
	if err == nil {
		t.Fatalf(`Parsing must fail`)
	}
}

func TestInvalidBaseURL(t *testing.T) {
	os.Clearenv()
	os.Setenv("BASE_URL", "http://example|org")
	cfg := NewConfig()

	if cfg.BaseURL() != defaultBaseURL {
		t.Fatalf(`Unexpected base URL, got "%s"`, cfg.BaseURL())
	}

	if cfg.RootURL() != defaultBaseURL {
		t.Fatalf(`Unexpected root URL, got "%s"`, cfg.RootURL())
	}

	if cfg.BasePath() != "" {
		t.Fatalf(`Unexpected base path, got "%s"`, cfg.BasePath())
	_, err := parse()
	if err == nil {
		t.Fatalf(`Parsing must fail`)
	}
}

func TestDefaultBaseURL(t *testing.T) {
	os.Clearenv()
	cfg := NewConfig()

	if cfg.BaseURL() != defaultBaseURL {
		t.Fatalf(`Unexpected base URL, got "%s"`, cfg.BaseURL())
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	if cfg.RootURL() != defaultBaseURL {
		t.Fatalf(`Unexpected root URL, got "%s"`, cfg.RootURL())
	if opts.BaseURL() != defaultBaseURL {
		t.Fatalf(`Unexpected base URL, got "%s"`, opts.BaseURL())
	}

	if cfg.BasePath() != "" {
		t.Fatalf(`Unexpected base path, got "%s"`, cfg.BasePath())
	if opts.RootURL() != defaultBaseURL {
		t.Fatalf(`Unexpected root URL, got "%s"`, opts.RootURL())
	}

	if opts.BasePath() != "" {
		t.Fatalf(`Unexpected base path, got "%s"`, opts.BasePath())
	}
}



@@ 208,9 135,13 @@ func TestDatabaseURL(t *testing.T) {
	os.Clearenv()
	os.Setenv("DATABASE_URL", "foobar")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := "foobar"
	result := cfg.DatabaseURL()
	result := opts.DatabaseURL()

	if result != expected {
		t.Fatalf(`Unexpected DATABASE_URL value, got %q instead of %q`, result, expected)


@@ 219,9 150,14 @@ func TestDatabaseURL(t *testing.T) {

func TestDefaultDatabaseURLValue(t *testing.T) {
	os.Clearenv()
	cfg := NewConfig()
	result := cfg.DatabaseURL()

	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultDatabaseURL
	result := opts.DatabaseURL()

	if result != expected {
		t.Fatalf(`Unexpected DATABASE_URL value, got %q instead of %q`, result, expected)


@@ 231,22 167,30 @@ func TestDefaultDatabaseURLValue(t *testing.T) {
func TestDefaultDatabaseMaxConnsValue(t *testing.T) {
	os.Clearenv()

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultDatabaseMaxConns
	result := cfg.DatabaseMaxConns()
	result := opts.DatabaseMaxConns()

	if result != expected {
		t.Fatalf(`Unexpected DATABASE_MAX_CONNS value, got %v instead of %v`, result, expected)
	}
}

func TestDeatabaseMaxConns(t *testing.T) {
func TestDatabaseMaxConns(t *testing.T) {
	os.Clearenv()
	os.Setenv("DATABASE_MAX_CONNS", "42")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := 42
	result := cfg.DatabaseMaxConns()
	result := opts.DatabaseMaxConns()

	if result != expected {
		t.Fatalf(`Unexpected DATABASE_MAX_CONNS value, got %v instead of %v`, result, expected)


@@ 256,9 200,13 @@ func TestDeatabaseMaxConns(t *testing.T) {
func TestDefaultDatabaseMinConnsValue(t *testing.T) {
	os.Clearenv()

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultDatabaseMinConns
	result := cfg.DatabaseMinConns()
	result := opts.DatabaseMinConns()

	if result != expected {
		t.Fatalf(`Unexpected DATABASE_MIN_CONNS value, got %v instead of %v`, result, expected)


@@ 269,9 217,13 @@ func TestDatabaseMinConns(t *testing.T) {
	os.Clearenv()
	os.Setenv("DATABASE_MIN_CONNS", "42")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := 42
	result := cfg.DatabaseMinConns()
	result := opts.DatabaseMinConns()

	if result != expected {
		t.Fatalf(`Unexpected DATABASE_MIN_CONNS value, got %v instead of %v`, result, expected)


@@ 282,9 234,13 @@ func TestListenAddr(t *testing.T) {
	os.Clearenv()
	os.Setenv("LISTEN_ADDR", "foobar")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := "foobar"
	result := cfg.ListenAddr()
	result := opts.ListenAddr()

	if result != expected {
		t.Fatalf(`Unexpected LISTEN_ADDR value, got %q instead of %q`, result, expected)


@@ 296,9 252,13 @@ func TestListenAddrWithPortDefined(t *testing.T) {
	os.Setenv("PORT", "3000")
	os.Setenv("LISTEN_ADDR", "foobar")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := ":3000"
	result := cfg.ListenAddr()
	result := opts.ListenAddr()

	if result != expected {
		t.Fatalf(`Unexpected LISTEN_ADDR value, got %q instead of %q`, result, expected)


@@ 307,9 267,14 @@ func TestListenAddrWithPortDefined(t *testing.T) {

func TestDefaultListenAddrValue(t *testing.T) {
	os.Clearenv()
	cfg := NewConfig()
	result := cfg.ListenAddr()

	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultListenAddr
	result := opts.ListenAddr()

	if result != expected {
		t.Fatalf(`Unexpected LISTEN_ADDR value, got %q instead of %q`, result, expected)


@@ 320,9 285,13 @@ func TestCertFile(t *testing.T) {
	os.Clearenv()
	os.Setenv("CERT_FILE", "foobar")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := "foobar"
	result := cfg.CertFile()
	result := opts.CertFile()

	if result != expected {
		t.Fatalf(`Unexpected CERT_FILE value, got %q instead of %q`, result, expected)


@@ 331,9 300,14 @@ func TestCertFile(t *testing.T) {

func TestDefaultCertFileValue(t *testing.T) {
	os.Clearenv()
	cfg := NewConfig()
	result := cfg.CertFile()

	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultCertFile
	result := opts.CertFile()

	if result != expected {
		t.Fatalf(`Unexpected CERT_FILE value, got %q instead of %q`, result, expected)


@@ 344,9 318,13 @@ func TestKeyFile(t *testing.T) {
	os.Clearenv()
	os.Setenv("KEY_FILE", "foobar")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := "foobar"
	result := cfg.KeyFile()
	result := opts.CertKeyFile()

	if result != expected {
		t.Fatalf(`Unexpected KEY_FILE value, got %q instead of %q`, result, expected)


@@ 355,9 333,14 @@ func TestKeyFile(t *testing.T) {

func TestDefaultKeyFileValue(t *testing.T) {
	os.Clearenv()
	cfg := NewConfig()
	result := cfg.KeyFile()

	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultKeyFile
	result := opts.CertKeyFile()

	if result != expected {
		t.Fatalf(`Unexpected KEY_FILE value, got %q instead of %q`, result, expected)


@@ 368,9 351,13 @@ func TestCertDomain(t *testing.T) {
	os.Clearenv()
	os.Setenv("CERT_DOMAIN", "example.org")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := "example.org"
	result := cfg.CertDomain()
	result := opts.CertDomain()

	if result != expected {
		t.Fatalf(`Unexpected CERT_DOMAIN value, got %q instead of %q`, result, expected)


@@ 379,9 366,14 @@ func TestCertDomain(t *testing.T) {

func TestDefaultCertDomainValue(t *testing.T) {
	os.Clearenv()
	cfg := NewConfig()
	result := cfg.CertDomain()

	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultCertDomain
	result := opts.CertDomain()

	if result != expected {
		t.Fatalf(`Unexpected CERT_DOMAIN value, got %q instead of %q`, result, expected)


@@ 392,9 384,13 @@ func TestCertCache(t *testing.T) {
	os.Clearenv()
	os.Setenv("CERT_CACHE", "foobar")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := "foobar"
	result := cfg.CertCache()
	result := opts.CertCache()

	if result != expected {
		t.Fatalf(`Unexpected CERT_CACHE value, got %q instead of %q`, result, expected)


@@ 403,9 399,14 @@ func TestCertCache(t *testing.T) {

func TestDefaultCertCacheValue(t *testing.T) {
	os.Clearenv()
	cfg := NewConfig()
	result := cfg.CertCache()

	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultCertCache
	result := opts.CertCache()

	if result != expected {
		t.Fatalf(`Unexpected CERT_CACHE value, got %q instead of %q`, result, expected)


@@ 415,9 416,13 @@ func TestDefaultCertCacheValue(t *testing.T) {
func TestDefaultCleanupFrequencyValue(t *testing.T) {
	os.Clearenv()

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultCleanupFrequency
	result := cfg.CleanupFrequency()
	result := opts.CleanupFrequency()

	if result != expected {
		t.Fatalf(`Unexpected CLEANUP_FREQUENCY value, got %v instead of %v`, result, expected)


@@ 428,9 433,13 @@ func TestCleanupFrequency(t *testing.T) {
	os.Clearenv()
	os.Setenv("CLEANUP_FREQUENCY", "42")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := 42
	result := cfg.CleanupFrequency()
	result := opts.CleanupFrequency()

	if result != expected {
		t.Fatalf(`Unexpected CLEANUP_FREQUENCY value, got %v instead of %v`, result, expected)


@@ 440,9 449,13 @@ func TestCleanupFrequency(t *testing.T) {
func TestDefaultWorkerPoolSizeValue(t *testing.T) {
	os.Clearenv()

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultWorkerPoolSize
	result := cfg.WorkerPoolSize()
	result := opts.WorkerPoolSize()

	if result != expected {
		t.Fatalf(`Unexpected WORKER_POOL_SIZE value, got %v instead of %v`, result, expected)


@@ 453,9 466,13 @@ func TestWorkerPoolSize(t *testing.T) {
	os.Clearenv()
	os.Setenv("WORKER_POOL_SIZE", "42")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := 42
	result := cfg.WorkerPoolSize()
	result := opts.WorkerPoolSize()

	if result != expected {
		t.Fatalf(`Unexpected WORKER_POOL_SIZE value, got %v instead of %v`, result, expected)


@@ 465,9 482,13 @@ func TestWorkerPoolSize(t *testing.T) {
func TestDefautPollingFrequencyValue(t *testing.T) {
	os.Clearenv()

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultPollingFrequency
	result := cfg.PollingFrequency()
	result := opts.PollingFrequency()

	if result != expected {
		t.Fatalf(`Unexpected POLLING_FREQUENCY value, got %v instead of %v`, result, expected)


@@ 478,9 499,13 @@ func TestPollingFrequency(t *testing.T) {
	os.Clearenv()
	os.Setenv("POLLING_FREQUENCY", "42")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := 42
	result := cfg.PollingFrequency()
	result := opts.PollingFrequency()

	if result != expected {
		t.Fatalf(`Unexpected POLLING_FREQUENCY value, got %v instead of %v`, result, expected)


@@ 490,9 515,13 @@ func TestPollingFrequency(t *testing.T) {
func TestDefaultBatchSizeValue(t *testing.T) {
	os.Clearenv()

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultBatchSize
	result := cfg.BatchSize()
	result := opts.BatchSize()

	if result != expected {
		t.Fatalf(`Unexpected BATCH_SIZE value, got %v instead of %v`, result, expected)


@@ 503,9 532,13 @@ func TestBatchSize(t *testing.T) {
	os.Clearenv()
	os.Setenv("BATCH_SIZE", "42")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := 42
	result := cfg.BatchSize()
	result := opts.BatchSize()

	if result != expected {
		t.Fatalf(`Unexpected BATCH_SIZE value, got %v instead of %v`, result, expected)


@@ 515,9 548,13 @@ func TestBatchSize(t *testing.T) {
func TestOAuth2UserCreationWhenUnset(t *testing.T) {
	os.Clearenv()

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := false
	result := cfg.IsOAuth2UserCreationAllowed()
	result := opts.IsOAuth2UserCreationAllowed()

	if result != expected {
		t.Fatalf(`Unexpected OAUTH2_USER_CREATION value, got %v instead of %v`, result, expected)


@@ 528,9 565,13 @@ func TestOAuth2UserCreationAdmin(t *testing.T) {
	os.Clearenv()
	os.Setenv("OAUTH2_USER_CREATION", "1")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := true
	result := cfg.IsOAuth2UserCreationAllowed()
	result := opts.IsOAuth2UserCreationAllowed()

	if result != expected {
		t.Fatalf(`Unexpected OAUTH2_USER_CREATION value, got %v instead of %v`, result, expected)


@@ 541,9 582,13 @@ func TestOAuth2ClientID(t *testing.T) {
	os.Clearenv()
	os.Setenv("OAUTH2_CLIENT_ID", "foobar")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := "foobar"
	result := cfg.OAuth2ClientID()
	result := opts.OAuth2ClientID()

	if result != expected {
		t.Fatalf(`Unexpected OAUTH2_CLIENT_ID value, got %q instead of %q`, result, expected)


@@ 552,9 597,14 @@ func TestOAuth2ClientID(t *testing.T) {

func TestDefaultOAuth2ClientIDValue(t *testing.T) {
	os.Clearenv()
	cfg := NewConfig()
	result := cfg.OAuth2ClientID()

	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultOAuth2ClientID
	result := opts.OAuth2ClientID()

	if result != expected {
		t.Fatalf(`Unexpected OAUTH2_CLIENT_ID value, got %q instead of %q`, result, expected)


@@ 565,9 615,13 @@ func TestOAuth2ClientSecret(t *testing.T) {
	os.Clearenv()
	os.Setenv("OAUTH2_CLIENT_SECRET", "secret")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := "secret"
	result := cfg.OAuth2ClientSecret()
	result := opts.OAuth2ClientSecret()

	if result != expected {
		t.Fatalf(`Unexpected OAUTH2_CLIENT_SECRET value, got %q instead of %q`, result, expected)


@@ 576,9 630,14 @@ func TestOAuth2ClientSecret(t *testing.T) {

func TestDefaultOAuth2ClientSecretValue(t *testing.T) {
	os.Clearenv()
	cfg := NewConfig()
	result := cfg.OAuth2ClientSecret()

	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultOAuth2ClientSecret
	result := opts.OAuth2ClientSecret()

	if result != expected {
		t.Fatalf(`Unexpected OAUTH2_CLIENT_SECRET value, got %q instead of %q`, result, expected)


@@ 589,9 648,13 @@ func TestOAuth2RedirectURL(t *testing.T) {
	os.Clearenv()
	os.Setenv("OAUTH2_REDIRECT_URL", "http://example.org")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := "http://example.org"
	result := cfg.OAuth2RedirectURL()
	result := opts.OAuth2RedirectURL()

	if result != expected {
		t.Fatalf(`Unexpected OAUTH2_REDIRECT_URL value, got %q instead of %q`, result, expected)


@@ 600,9 663,14 @@ func TestOAuth2RedirectURL(t *testing.T) {

func TestDefaultOAuth2RedirectURLValue(t *testing.T) {
	os.Clearenv()
	cfg := NewConfig()
	result := cfg.OAuth2RedirectURL()

	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultOAuth2RedirectURL
	result := opts.OAuth2RedirectURL()

	if result != expected {
		t.Fatalf(`Unexpected OAUTH2_REDIRECT_URL value, got %q instead of %q`, result, expected)


@@ 613,9 681,13 @@ func TestOAuth2Provider(t *testing.T) {
	os.Clearenv()
	os.Setenv("OAUTH2_PROVIDER", "google")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := "google"
	result := cfg.OAuth2Provider()
	result := opts.OAuth2Provider()

	if result != expected {
		t.Fatalf(`Unexpected OAUTH2_PROVIDER value, got %q instead of %q`, result, expected)


@@ 624,9 696,14 @@ func TestOAuth2Provider(t *testing.T) {

func TestDefaultOAuth2ProviderValue(t *testing.T) {
	os.Clearenv()
	cfg := NewConfig()
	result := cfg.OAuth2Provider()

	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultOAuth2Provider
	result := opts.OAuth2Provider()

	if result != expected {
		t.Fatalf(`Unexpected OAUTH2_PROVIDER value, got %q instead of %q`, result, expected)


@@ 636,9 713,13 @@ func TestDefaultOAuth2ProviderValue(t *testing.T) {
func TestHSTSWhenUnset(t *testing.T) {
	os.Clearenv()

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := true
	result := cfg.HasHSTS()
	result := opts.HasHSTS()

	if result != expected {
		t.Fatalf(`Unexpected DISABLE_HSTS value, got %v instead of %v`, result, expected)


@@ 649,9 730,13 @@ func TestHSTS(t *testing.T) {
	os.Clearenv()
	os.Setenv("DISABLE_HSTS", "1")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := false
	result := cfg.HasHSTS()
	result := opts.HasHSTS()

	if result != expected {
		t.Fatalf(`Unexpected DISABLE_HSTS value, got %v instead of %v`, result, expected)


@@ 661,9 746,13 @@ func TestHSTS(t *testing.T) {
func TestDisableHTTPServiceWhenUnset(t *testing.T) {
	os.Clearenv()

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := true
	result := cfg.HasHTTPService()
	result := opts.HasHTTPService()

	if result != expected {
		t.Fatalf(`Unexpected DISABLE_HTTP_SERVICE value, got %v instead of %v`, result, expected)


@@ 674,9 763,13 @@ func TestDisableHTTPService(t *testing.T) {
	os.Clearenv()
	os.Setenv("DISABLE_HTTP_SERVICE", "1")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := false
	result := cfg.HasHTTPService()
	result := opts.HasHTTPService()

	if result != expected {
		t.Fatalf(`Unexpected DISABLE_HTTP_SERVICE value, got %v instead of %v`, result, expected)


@@ 686,9 779,13 @@ func TestDisableHTTPService(t *testing.T) {
func TestDisableSchedulerServiceWhenUnset(t *testing.T) {
	os.Clearenv()

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := true
	result := cfg.HasSchedulerService()
	result := opts.HasSchedulerService()

	if result != expected {
		t.Fatalf(`Unexpected DISABLE_SCHEDULER_SERVICE value, got %v instead of %v`, result, expected)


@@ 699,9 796,13 @@ func TestDisableSchedulerService(t *testing.T) {
	os.Clearenv()
	os.Setenv("DISABLE_SCHEDULER_SERVICE", "1")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := false
	result := cfg.HasSchedulerService()
	result := opts.HasSchedulerService()

	if result != expected {
		t.Fatalf(`Unexpected DISABLE_SCHEDULER_SERVICE value, got %v instead of %v`, result, expected)


@@ 712,9 813,13 @@ func TestArchiveReadDays(t *testing.T) {
	os.Clearenv()
	os.Setenv("ARCHIVE_READ_DAYS", "7")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := 7
	result := cfg.ArchiveReadDays()
	result := opts.ArchiveReadDays()

	if result != expected {
		t.Fatalf(`Unexpected ARCHIVE_READ_DAYS value, got %v instead of %v`, result, expected)


@@ 724,9 829,13 @@ func TestArchiveReadDays(t *testing.T) {
func TestRunMigrationsWhenUnset(t *testing.T) {
	os.Clearenv()

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := false
	result := cfg.RunMigrations()
	result := opts.RunMigrations()

	if result != expected {
		t.Fatalf(`Unexpected RUN_MIGRATIONS value, got %v instead of %v`, result, expected)


@@ 737,9 846,13 @@ func TestRunMigrations(t *testing.T) {
	os.Clearenv()
	os.Setenv("RUN_MIGRATIONS", "yes")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := true
	result := cfg.RunMigrations()
	result := opts.RunMigrations()

	if result != expected {
		t.Fatalf(`Unexpected RUN_MIGRATIONS value, got %v instead of %v`, result, expected)


@@ 749,9 862,13 @@ func TestRunMigrations(t *testing.T) {
func TestCreateAdminWhenUnset(t *testing.T) {
	os.Clearenv()

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := false
	result := cfg.CreateAdmin()
	result := opts.CreateAdmin()

	if result != expected {
		t.Fatalf(`Unexpected CREATE_ADMIN value, got %v instead of %v`, result, expected)


@@ 762,9 879,13 @@ func TestCreateAdmin(t *testing.T) {
	os.Clearenv()
	os.Setenv("CREATE_ADMIN", "true")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := true
	result := cfg.CreateAdmin()
	result := opts.CreateAdmin()

	if result != expected {
		t.Fatalf(`Unexpected CREATE_ADMIN value, got %v instead of %v`, result, expected)


@@ 775,9 896,13 @@ func TestPocketConsumerKeyFromEnvVariable(t *testing.T) {
	os.Clearenv()
	os.Setenv("POCKET_CONSUMER_KEY", "something")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := "something"
	result := cfg.PocketConsumerKey("default")
	result := opts.PocketConsumerKey("default")

	if result != expected {
		t.Fatalf(`Unexpected POCKET_CONSUMER_KEY value, got %q instead of %q`, result, expected)


@@ 787,9 912,13 @@ func TestPocketConsumerKeyFromEnvVariable(t *testing.T) {
func TestPocketConsumerKeyFromUserPrefs(t *testing.T) {
	os.Clearenv()

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := "default"
	result := cfg.PocketConsumerKey("default")
	result := opts.PocketConsumerKey("default")

	if result != expected {
		t.Fatalf(`Unexpected POCKET_CONSUMER_KEY value, got %q instead of %q`, result, expected)


@@ 800,9 929,13 @@ func TestProxyImages(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "all")

	cfg := NewConfig()
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := "all"
	result := cfg.ProxyImages()
	result := opts.ProxyImages()

	if result != expected {
		t.Fatalf(`Unexpected PROXY_IMAGES value, got %q instead of %q`, result, expected)


@@ 811,9 944,14 @@ func TestProxyImages(t *testing.T) {

func TestDefaultProxyImagesValue(t *testing.T) {
	os.Clearenv()
	cfg := NewConfig()
	result := cfg.ProxyImages()

	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	expected := defaultProxyImages
	result := opts.ProxyImages()

	if result != expected {
		t.Fatalf(`Unexpected PROXY_IMAGES value, got %q instead of %q`, result, expected)


@@ 822,19 960,27 @@ func TestDefaultProxyImagesValue(t *testing.T) {

func TestHTTPSOff(t *testing.T) {
	os.Clearenv()
	cfg := NewConfig()

	if cfg.IsHTTPS {
		t.Fatalf(`Unexpected HTTPS value, got "%v"`, cfg.IsHTTPS)
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	if opts.HTTPS {
		t.Fatalf(`Unexpected HTTPS value, got "%v"`, opts.HTTPS)
	}
}

func TestHTTPSOn(t *testing.T) {
	os.Clearenv()
	os.Setenv("HTTPS", "on")
	cfg := NewConfig()

	if !cfg.IsHTTPS {
		t.Fatalf(`Unexpected HTTPS value, got "%v"`, cfg.IsHTTPS)
	opts, err := parse()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
	}

	if !opts.HTTPS {
		t.Fatalf(`Unexpected HTTPS value, got "%v"`, opts.HTTPS)
	}
}

M config/doc.go => config/doc.go +2 -2
@@ 1,10 1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Copyright 2019 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

/*

Package config handles configuration values for the application.
Package config handles configuration management for the application.

*/
package config // import "miniflux.app/config"

A config/options.go => config/options.go +214 -0
@@ 0,0 1,214 @@
// Copyright 2019 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

package config // import "miniflux.app/config"

const (
	defaultBaseURL            = "http://localhost"
	defaultWorkerPoolSize     = 5
	defaultPollingFrequency   = 60
	defaultBatchSize          = 10
	defaultDatabaseURL        = "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
	defaultDatabaseMaxConns   = 20
	defaultDatabaseMinConns   = 1
	defaultArchiveReadDays    = 60
	defaultListenAddr         = "127.0.0.1:8080"
	defaultCertFile           = ""
	defaultKeyFile            = ""
	defaultCertDomain         = ""
	defaultCertCache          = "/tmp/cert_cache"
	defaultCleanupFrequency   = 24
	defaultProxyImages        = "http-only"
	defaultOAuth2ClientID     = ""
	defaultOAuth2ClientSecret = ""
	defaultOAuth2RedirectURL  = ""
	defaultOAuth2Provider     = ""
)

// Options contains configuration options.
type Options struct {
	HTTPS                     bool
	hsts                      bool
	httpService               bool
	schedulerService          bool
	debug                     bool
	baseURL                   string
	rootURL                   string
	basePath                  string
	databaseURL               string
	databaseMaxConns          int
	databaseMinConns          int
	runMigrations             bool
	listenAddr                string
	certFile                  string
	certDomain                string
	certCache                 string
	certKeyFile               string
	cleanupFrequency          int
	archiveReadDays           int
	pollingFrequency          int
	batchSize                 int
	workerPoolSize            int
	createAdmin               bool
	proxyImages               string
	oauth2UserCreationAllowed bool
	oauth2ClientID            string
	oauth2ClientSecret        string
	oauth2RedirectURL         string
	oauth2Provider            string
	pocketConsumerKey         string
}

// HasDebugMode returns true if debug mode is enabled.
func (o *Options) HasDebugMode() bool {
	return o.debug
}

// BaseURL returns the application base URL with path.
func (o *Options) BaseURL() string {
	return o.baseURL
}

// RootURL returns the base URL without path.
func (o *Options) RootURL() string {
	return o.rootURL
}

// BasePath returns the application base path according to the base URL.
func (o *Options) BasePath() string {
	return o.basePath
}

// IsDefaultDatabaseURL returns true if the default database URL is used.
func (o *Options) IsDefaultDatabaseURL() bool {
	return o.databaseURL == defaultDatabaseURL
}

// DatabaseURL returns the database URL.
func (o *Options) DatabaseURL() string {
	return o.databaseURL
}

// DatabaseMaxConns returns the maximum number of database connections.
func (o *Options) DatabaseMaxConns() int {
	return o.databaseMaxConns
}

// DatabaseMinConns returns the minimum number of database connections.
func (o *Options) DatabaseMinConns() int {
	return o.databaseMinConns
}

// ListenAddr returns the listen address for the HTTP server.
func (o *Options) ListenAddr() string {
	return o.listenAddr
}

// CertFile returns the SSL certificate filename if any.
func (o *Options) CertFile() string {
	return o.certFile
}

// CertKeyFile returns the private key filename for custom SSL certificate.
func (o *Options) CertKeyFile() string {
	return o.certKeyFile
}

// CertDomain returns the domain to use for Let's Encrypt certificate.
func (o *Options) CertDomain() string {
	return o.certDomain
}

// CertCache returns the directory to use for Let's Encrypt session cache.
func (o *Options) CertCache() string {
	return o.certCache
}

// CleanupFrequency returns the interval for cleanup jobs.
func (o *Options) CleanupFrequency() int {
	return o.cleanupFrequency
}

// WorkerPoolSize returns the number of background worker.
func (o *Options) WorkerPoolSize() int {
	return o.workerPoolSize
}

// PollingFrequency returns the interval to refresh feeds in the background.
func (o *Options) PollingFrequency() int {
	return o.pollingFrequency
}

// BatchSize returns the number of feeds to send for background processing.
func (o *Options) BatchSize() int {
	return o.batchSize
}

// IsOAuth2UserCreationAllowed returns true if user creation is allowed for OAuth2 users.
func (o *Options) IsOAuth2UserCreationAllowed() bool {
	return o.oauth2UserCreationAllowed
}

// OAuth2ClientID returns the OAuth2 Client ID.
func (o *Options) OAuth2ClientID() string {
	return o.oauth2ClientID
}

// OAuth2ClientSecret returns the OAuth2 client secret.
func (o *Options) OAuth2ClientSecret() string {
	return o.oauth2ClientSecret
}

// OAuth2RedirectURL returns the OAuth2 redirect URL.
func (o *Options) OAuth2RedirectURL() string {
	return o.oauth2RedirectURL
}

// OAuth2Provider returns the name of the OAuth2 provider configured.
func (o *Options) OAuth2Provider() string {
	return o.oauth2Provider
}

// HasHSTS returns true if HTTP Strict Transport Security is enabled.
func (o *Options) HasHSTS() bool {
	return o.hsts
}

// RunMigrations returns true if the environment variable RUN_MIGRATIONS is not empty.
func (o *Options) RunMigrations() bool {
	return o.runMigrations
}

// CreateAdmin returns true if the environment variable CREATE_ADMIN is not empty.
func (o *Options) CreateAdmin() bool {
	return o.createAdmin
}

// ProxyImages returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
func (o *Options) ProxyImages() string {
	return o.proxyImages
}

// HasHTTPService returns true if the HTTP service is enabled.
func (o *Options) HasHTTPService() bool {
	return o.httpService
}

// HasSchedulerService returns true if the scheduler service is enabled.
func (o *Options) HasSchedulerService() bool {
	return o.schedulerService
}

// ArchiveReadDays returns the number of days after which marking read items as removed.
func (o *Options) ArchiveReadDays() int {
	return o.archiveReadDays
}

// PocketConsumerKey returns the Pocket Consumer Key if configured.
func (o *Options) PocketConsumerKey(defaultValue string) string {
	if o.pocketConsumerKey != "" {
		return o.pocketConsumerKey
	}
	return defaultValue
}

A config/parser.go => config/parser.go +124 -0
@@ 0,0 1,124 @@
// Copyright 2019 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

package config // import "miniflux.app/config"

import (
	"errors"
	"fmt"
	"net/url"
	"os"
	"strconv"
	"strings"
)

func parse() (opts *Options, err error) {
	opts = &Options{}
	opts.baseURL, opts.rootURL, opts.basePath, err = parseBaseURL()
	if err != nil {
		return nil, err
	}

	opts.debug = getBooleanValue("DEBUG")
	opts.listenAddr = parseListenAddr()

	opts.databaseURL = getStringValue("DATABASE_URL", defaultDatabaseURL)
	opts.databaseMaxConns = getIntValue("DATABASE_MAX_CONNS", defaultDatabaseMaxConns)
	opts.databaseMinConns = getIntValue("DATABASE_MIN_CONNS", defaultDatabaseMinConns)
	opts.runMigrations = getBooleanValue("RUN_MIGRATIONS")

	opts.hsts = !getBooleanValue("DISABLE_HSTS")
	opts.HTTPS = getBooleanValue("HTTPS")

	opts.schedulerService = !getBooleanValue("DISABLE_SCHEDULER_SERVICE")
	opts.httpService = !getBooleanValue("DISABLE_HTTP_SERVICE")

	opts.certFile = getStringValue("CERT_FILE", defaultCertFile)
	opts.certKeyFile = getStringValue("KEY_FILE", defaultKeyFile)
	opts.certDomain = getStringValue("CERT_DOMAIN", defaultCertDomain)
	opts.certCache = getStringValue("CERT_CACHE", defaultCertCache)

	opts.cleanupFrequency = getIntValue("CLEANUP_FREQUENCY", defaultCleanupFrequency)
	opts.workerPoolSize = getIntValue("WORKER_POOL_SIZE", defaultWorkerPoolSize)
	opts.pollingFrequency = getIntValue("POLLING_FREQUENCY", defaultPollingFrequency)
	opts.batchSize = getIntValue("BATCH_SIZE", defaultBatchSize)
	opts.archiveReadDays = getIntValue("ARCHIVE_READ_DAYS", defaultArchiveReadDays)
	opts.proxyImages = getStringValue("PROXY_IMAGES", defaultProxyImages)

	opts.oauth2UserCreationAllowed = getBooleanValue("OAUTH2_USER_CREATION")
	opts.oauth2ClientID = getStringValue("OAUTH2_CLIENT_ID", defaultOAuth2ClientID)
	opts.oauth2ClientSecret = getStringValue("OAUTH2_CLIENT_SECRET", defaultOAuth2ClientSecret)
	opts.oauth2RedirectURL = getStringValue("OAUTH2_REDIRECT_URL", defaultOAuth2RedirectURL)
	opts.oauth2Provider = getStringValue("OAUTH2_PROVIDER", defaultOAuth2Provider)

	opts.pocketConsumerKey = getStringValue("POCKET_CONSUMER_KEY", "")

	opts.createAdmin = getBooleanValue("CREATE_ADMIN")

	return opts, nil
}

func parseBaseURL() (string, string, string, error) {
	baseURL := os.Getenv("BASE_URL")
	if baseURL == "" {
		return defaultBaseURL, defaultBaseURL, "", nil
	}

	if baseURL[len(baseURL)-1:] == "/" {
		baseURL = baseURL[:len(baseURL)-1]
	}

	u, err := url.Parse(baseURL)
	if err != nil {
		return "", "", "", fmt.Errorf("Invalid BASE_URL: %v", err)
	}

	scheme := strings.ToLower(u.Scheme)
	if scheme != "https" && scheme != "http" {
		return "", "", "", errors.New("Invalid BASE_URL: scheme must be http or https")
	}

	basePath := u.Path
	u.Path = ""
	return baseURL, u.String(), basePath, nil
}

func parseListenAddr() string {
	if port := os.Getenv("PORT"); port != "" {
		return ":" + port
	}

	return getStringValue("LISTEN_ADDR", defaultListenAddr)
}

func getBooleanValue(key string) bool {
	value := strings.ToLower(os.Getenv(key))
	if value == "1" || value == "yes" || value == "true" || value == "on" {
		return true
	}
	return false
}

func getStringValue(key, fallback string) string {
	value := os.Getenv(key)
	if value == "" {
		return fallback
	}

	return value
}

func getIntValue(key string, fallback int) int {
	value := os.Getenv(key)
	if value == "" {
		return fallback
	}

	v, err := strconv.Atoi(value)
	if err != nil {
		return fallback
	}

	return v
}

A config/parser_test.go => config/parser_test.go +79 -0
@@ 0,0 1,79 @@
// Copyright 2019 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

package config // import "miniflux.app/config"

import (
	"os"
	"testing"
)

func TestGetBooleanValueWithUnsetVariable(t *testing.T) {
	os.Clearenv()
	if getBooleanValue("MY_TEST_VARIABLE") {
		t.Errorf(`Unset variables should returns false`)
	}
}

func TestGetBooleanValue(t *testing.T) {
	scenarios := map[string]bool{
		"":        false,
		"1":       true,
		"Yes":     true,
		"yes":     true,
		"True":    true,
		"true":    true,
		"on":      true,
		"false":   false,
		"off":     false,
		"invalid": false,
	}

	for input, expected := range scenarios {
		os.Clearenv()
		os.Setenv("MY_TEST_VARIABLE", input)
		result := getBooleanValue("MY_TEST_VARIABLE")
		if result != expected {
			t.Errorf(`Unexpected result for %q, got %v instead of %v`, input, result, expected)
		}
	}
}

func TestGetStringValueWithUnsetVariable(t *testing.T) {
	os.Clearenv()
	if getStringValue("MY_TEST_VARIABLE", "defaultValue") != "defaultValue" {
		t.Errorf(`Unset variables should returns the default value`)
	}
}

func TestGetStringValue(t *testing.T) {
	os.Clearenv()
	os.Setenv("MY_TEST_VARIABLE", "test")
	if getStringValue("MY_TEST_VARIABLE", "defaultValue") != "test" {
		t.Errorf(`Defined variables should returns the specified value`)
	}
}

func TestGetIntValueWithUnsetVariable(t *testing.T) {
	os.Clearenv()
	if getIntValue("MY_TEST_VARIABLE", 42) != 42 {
		t.Errorf(`Unset variables should returns the default value`)
	}
}

func TestGetIntValueWithInvalidInput(t *testing.T) {
	os.Clearenv()
	os.Setenv("MY_TEST_VARIABLE", "invalid integer")
	if getIntValue("MY_TEST_VARIABLE", 42) != 42 {
		t.Errorf(`Invalid integer should returns the default value`)
	}
}

func TestGetIntValue(t *testing.T) {
	os.Clearenv()
	os.Setenv("MY_TEST_VARIABLE", "2018")
	if getIntValue("MY_TEST_VARIABLE", 42) != 2018 {
		t.Errorf(`Defined variables should returns the specified value`)
	}
}

M fever/handler.go => fever/handler.go +3 -5
@@ 10,7 10,6 @@ import (
	"strings"
	"time"

	"miniflux.app/config"
	"miniflux.app/http/request"
	"miniflux.app/http/response/json"
	"miniflux.app/integration"


@@ 22,8 21,8 @@ import (
)

// Serve handles Fever API calls.
func Serve(router *mux.Router, cfg *config.Config, store *storage.Storage) {
	handler := &handler{cfg, store}
func Serve(router *mux.Router, store *storage.Storage) {
	handler := &handler{store}

	sr := router.PathPrefix("/fever").Subrouter()
	sr.Use(newMiddleware(store).serve)


@@ 31,7 30,6 @@ func Serve(router *mux.Router, cfg *config.Config, store *storage.Storage) {
}

type handler struct {
	cfg   *config.Config
	store *storage.Storage
}



@@ 424,7 422,7 @@ func (h *handler) handleWriteItems(w http.ResponseWriter, r *http.Request) {
		}

		go func() {
			integration.SendEntry(h.cfg, entry, settings)
			integration.SendEntry(entry, settings)
		}()
	}


M integration/integration.go => integration/integration.go +2 -2
@@ 16,7 16,7 @@ import (
)

// SendEntry send the entry to the activated providers.
func SendEntry(cfg *config.Config, entry *model.Entry, integration *model.Integration) {
func SendEntry(entry *model.Entry, integration *model.Integration) {
	if integration.PinboardEnabled {
		client := pinboard.NewClient(integration.PinboardToken)
		err := client.AddBookmark(


@@ 64,7 64,7 @@ func SendEntry(cfg *config.Config, entry *model.Entry, integration *model.Integr
	}

	if integration.PocketEnabled {
		client := pocket.NewClient(cfg.PocketConsumerKey(integration.PocketConsumerKey), integration.PocketAccessToken)
		client := pocket.NewClient(config.Opts.PocketConsumerKey(integration.PocketConsumerKey), integration.PocketAccessToken)
		if err := client.AddURL(entry.URL, entry.Title); err != nil {
			logger.Error("[Integration] UserID #%d: %v", integration.UserID, err)
		}

M service/httpd/httpd.go => service/httpd/httpd.go +15 -15
@@ 27,17 27,17 @@ import (
)

// Serve starts a new HTTP server.
func Serve(cfg *config.Config, store *storage.Storage, pool *worker.Pool, feedHandler *feed.Handler) *http.Server {
	certFile := cfg.CertFile()
	keyFile := cfg.KeyFile()
	certDomain := cfg.CertDomain()
	certCache := cfg.CertCache()
	listenAddr := cfg.ListenAddr()
func Serve(store *storage.Storage, pool *worker.Pool, feedHandler *feed.Handler) *http.Server {
	certFile := config.Opts.CertFile()
	keyFile := config.Opts.CertKeyFile()
	certDomain := config.Opts.CertDomain()
	certCache := config.Opts.CertCache()
	listenAddr := config.Opts.ListenAddr()
	server := &http.Server{
		ReadTimeout:  30 * time.Second,
		WriteTimeout: 30 * time.Second,
		IdleTimeout:  60 * time.Second,
		Handler:      setupHandler(cfg, store, feedHandler, pool),
		Handler:      setupHandler(store, feedHandler, pool),
	}

	switch {


@@ 46,10 46,10 @@ func Serve(cfg *config.Config, store *storage.Storage, pool *worker.Pool, feedHa
	case strings.HasPrefix(listenAddr, "/"):
		startUnixSocketServer(server, listenAddr)
	case certDomain != "" && certCache != "":
		cfg.IsHTTPS = true
		config.Opts.HTTPS = true
		startAutoCertTLSServer(server, certDomain, certCache)
	case certFile != "" && keyFile != "":
		cfg.IsHTTPS = true
		config.Opts.HTTPS = true
		server.Addr = listenAddr
		startTLSServer(server, certFile, keyFile)
	default:


@@ 156,18 156,18 @@ func startHTTPServer(server *http.Server) {
	}()
}

func setupHandler(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handler, pool *worker.Pool) *mux.Router {
func setupHandler(store *storage.Storage, feedHandler *feed.Handler, pool *worker.Pool) *mux.Router {
	router := mux.NewRouter()

	if cfg.BasePath() != "" {
		router = router.PathPrefix(cfg.BasePath()).Subrouter()
	if config.Opts.BasePath() != "" {
		router = router.PathPrefix(config.Opts.BasePath()).Subrouter()
	}

	router.Use(newMiddleware(cfg).Serve)
	router.Use(middleware)

	fever.Serve(router, cfg, store)
	fever.Serve(router, store)
	api.Serve(router, store, feedHandler)
	ui.Serve(router, cfg, store, pool, feedHandler)
	ui.Serve(router, store, pool, feedHandler)

	router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("OK"))

M service/httpd/middleware.go => service/httpd/middleware.go +4 -12
@@ 13,32 13,24 @@ import (
	"miniflux.app/logger"
)

type middleware struct {
	cfg *config.Config
}

func newMiddleware(cfg *config.Config) *middleware {
	return &middleware{cfg}
}

func (m *middleware) Serve(next http.Handler) http.Handler {
func middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		clientIP := request.FindClientIP(r)
		ctx := r.Context()
		ctx = context.WithValue(ctx, request.ClientIPContextKey, clientIP)

		if r.Header.Get("X-Forwarded-Proto") == "https" {
			m.cfg.IsHTTPS = true
			config.Opts.HTTPS = true
		}

		protocol := "HTTP"
		if m.cfg.IsHTTPS {
		if config.Opts.HTTPS {
			protocol = "HTTPS"
		}

		logger.Debug("[%s] %s %s %s", protocol, clientIP, r.Method, r.RequestURI)

		if m.cfg.IsHTTPS && m.cfg.HasHSTS() {
		if config.Opts.HTTPS && config.Opts.HasHSTS() {
			w.Header().Set("Strict-Transport-Security", "max-age=31536000")
		}


M service/scheduler/scheduler.go => service/scheduler/scheduler.go +3 -3
@@ 14,10 14,10 @@ import (
)

// Serve starts the internal scheduler.
func Serve(cfg *config.Config, store *storage.Storage, pool *worker.Pool) {
func Serve(store *storage.Storage, pool *worker.Pool) {
	logger.Info(`Starting scheduler...`)
	go feedScheduler(store, pool, cfg.PollingFrequency(), cfg.BatchSize())
	go cleanupScheduler(store, cfg.CleanupFrequency(), cfg.ArchiveReadDays())
	go feedScheduler(store, pool, config.Opts.PollingFrequency(), config.Opts.BatchSize())
	go cleanupScheduler(store, config.Opts.CleanupFrequency(), config.Opts.ArchiveReadDays())
}

func feedScheduler(store *storage.Storage, pool *worker.Pool, frequency, batchSize int) {

M template/engine.go => template/engine.go +2 -3
@@ 9,7 9,6 @@ import (
	"html/template"
	"time"

	"miniflux.app/config"
	"miniflux.app/errors"
	"miniflux.app/locale"
	"miniflux.app/logger"


@@ 78,10 77,10 @@ func (e *Engine) Render(name, language string, data interface{}) []byte {
}

// NewEngine returns a new template engine.
func NewEngine(cfg *config.Config, router *mux.Router) *Engine {
func NewEngine(router *mux.Router) *Engine {
	tpl := &Engine{
		templates: make(map[string]*template.Template),
		funcMap:   newFuncMap(cfg, router),
		funcMap:   &funcMap{router},
	}

	tpl.parseAll()

M template/functions.go => template/functions.go +9 -14
@@ 7,8 7,8 @@ package template // import "miniflux.app/template"
import (
	"encoding/base64"
	"fmt"
	"math"
	"html/template"
	"math"
	"net/mail"
	"strings"
	"time"


@@ 20,12 20,11 @@ import (
	"miniflux.app/timezone"
	"miniflux.app/url"

	"github.com/gorilla/mux"
	"github.com/PuerkitoBio/goquery"
	"github.com/gorilla/mux"
)

type funcMap struct {
	cfg    *config.Config
	router *mux.Router
}



@@ 37,13 36,13 @@ func (f *funcMap) Map() template.FuncMap {
		"truncate": truncate,
		"isEmail":  isEmail,
		"baseURL": func() string {
			return f.cfg.BaseURL()
			return config.Opts.BaseURL()
		},
		"rootURL": func() string {
			return f.cfg.RootURL()
			return config.Opts.RootURL()
		},
		"hasOAuth2Provider": func(provider string) bool {
			return f.cfg.OAuth2Provider() == provider
			return config.Opts.OAuth2Provider() == provider
		},
		"route": func(name string, args ...interface{}) string {
			return route.Path(f.router, name, args...)


@@ 52,10 51,10 @@ func (f *funcMap) Map() template.FuncMap {
			return template.HTML(str)
		},
		"proxyFilter": func(data string) string {
			return imageProxyFilter(f.router, f.cfg, data)
			return imageProxyFilter(f.router, data)
		},
		"proxyURL": func(link string) string {
			proxyImages := f.cfg.ProxyImages()
			proxyImages := config.Opts.ProxyImages()

			if proxyImages == "all" || (proxyImages != "none" && !url.IsHTTPS(link)) {
				return proxify(f.router, link)


@@ 92,10 91,6 @@ func (f *funcMap) Map() template.FuncMap {
	}
}

func newFuncMap(cfg *config.Config, router *mux.Router) *funcMap {
	return &funcMap{cfg, router}
}

func dict(values ...interface{}) (map[string]interface{}, error) {
	if len(values)%2 != 0 {
		return nil, fmt.Errorf("dict expects an even number of arguments")


@@ 178,8 173,8 @@ func elapsedTime(printer *locale.Printer, tz string, t time.Time) string {
	}
}

func imageProxyFilter(router *mux.Router, cfg *config.Config, data string) string {
	proxyImages := cfg.ProxyImages()
func imageProxyFilter(router *mux.Router, data string) string {
	proxyImages := config.Opts.ProxyImages()
	if proxyImages == "none" {
		return data
	}

M template/functions_test.go => template/functions_test.go +16 -16
@@ 134,13 134,13 @@ func TestElapsedTime(t *testing.T) {
func TestProxyFilterWithHttpDefault(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "http-only")
	c := config.NewConfig()
	config.ParseConfig()

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")

	input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
	output := imageProxyFilter(r, c, input)
	output := imageProxyFilter(r, input)
	expected := `<p><img src="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`

	if expected != output {


@@ 151,13 151,13 @@ func TestProxyFilterWithHttpDefault(t *testing.T) {
func TestProxyFilterWithHttpsDefault(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "http-only")
	c := config.NewConfig()
	config.ParseConfig()

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")

	input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
	output := imageProxyFilter(r, c, input)
	output := imageProxyFilter(r, input)
	expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`

	if expected != output {


@@ 168,13 168,13 @@ func TestProxyFilterWithHttpsDefault(t *testing.T) {
func TestProxyFilterWithHttpNever(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "none")
	c := config.NewConfig()
	config.ParseConfig()

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")

	input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
	output := imageProxyFilter(r, c, input)
	output := imageProxyFilter(r, input)
	expected := input

	if expected != output {


@@ 185,13 185,13 @@ func TestProxyFilterWithHttpNever(t *testing.T) {
func TestProxyFilterWithHttpsNever(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "none")
	c := config.NewConfig()
	config.ParseConfig()

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")

	input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
	output := imageProxyFilter(r, c, input)
	output := imageProxyFilter(r, input)
	expected := input

	if expected != output {


@@ 202,13 202,13 @@ func TestProxyFilterWithHttpsNever(t *testing.T) {
func TestProxyFilterWithHttpAlways(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "all")
	c := config.NewConfig()
	config.ParseConfig()

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")

	input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
	output := imageProxyFilter(r, c, input)
	output := imageProxyFilter(r, input)
	expected := `<p><img src="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`

	if expected != output {


@@ 219,13 219,13 @@ func TestProxyFilterWithHttpAlways(t *testing.T) {
func TestProxyFilterWithHttpsAlways(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "all")
	c := config.NewConfig()
	config.ParseConfig()

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")

	input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
	output := imageProxyFilter(r, c, input)
	output := imageProxyFilter(r, input)
	expected := `<p><img src="/proxy/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`

	if expected != output {


@@ 236,13 236,13 @@ func TestProxyFilterWithHttpsAlways(t *testing.T) {
func TestProxyFilterWithHttpInvalid(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "invalid")
	c := config.NewConfig()
	config.ParseConfig()

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")

	input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
	output := imageProxyFilter(r, c, input)
	output := imageProxyFilter(r, input)
	expected := `<p><img src="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`

	if expected != output {


@@ 253,13 253,13 @@ func TestProxyFilterWithHttpInvalid(t *testing.T) {
func TestProxyFilterWithHttpsInvalid(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "invalid")
	c := config.NewConfig()
	config.ParseConfig()

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")

	input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
	output := imageProxyFilter(r, c, input)
	output := imageProxyFilter(r, input)
	expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`

	if expected != output {

M ui/entry_save.go => ui/entry_save.go +1 -1
@@ 37,7 37,7 @@ func (h *handler) saveEntry(w http.ResponseWriter, r *http.Request) {
	}

	go func() {
		integration.SendEntry(h.cfg, entry, settings)
		integration.SendEntry(entry, settings)
	}()

	json.Created(w, r, map[string]string{"message": "saved"})

M ui/handler.go => ui/handler.go +1 -3
@@ 2,10 2,9 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

package ui  // import "miniflux.app/ui"
package ui // import "miniflux.app/ui"

import (
	"miniflux.app/config"
	"miniflux.app/reader/feed"
	"miniflux.app/storage"
	"miniflux.app/template"


@@ 16,7 15,6 @@ import (

type handler struct {
	router      *mux.Router
	cfg         *config.Config
	store       *storage.Storage
	tpl         *template.Engine
	pool        *worker.Pool

M ui/integration_pocket.go => ui/integration_pocket.go +6 -5
@@ 2,13 2,14 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

package ui  // import "miniflux.app/ui"
package ui // import "miniflux.app/ui"

import (
	"net/http"

	"miniflux.app/http/response/html"
	"miniflux.app/config"
	"miniflux.app/http/request"
	"miniflux.app/http/response/html"
	"miniflux.app/http/route"
	"miniflux.app/integration/pocket"
	"miniflux.app/locale"


@@ 31,8 32,8 @@ func (h *handler) pocketAuthorize(w http.ResponseWriter, r *http.Request) {
	}

	sess := session.New(h.store, request.SessionID(r))
	connector := pocket.NewConnector(h.cfg.PocketConsumerKey(integration.PocketConsumerKey))
	redirectURL := h.cfg.BaseURL() + route.Path(h.router, "pocketCallback")
	connector := pocket.NewConnector(config.Opts.PocketConsumerKey(integration.PocketConsumerKey))
	redirectURL := config.Opts.BaseURL() + route.Path(h.router, "pocketCallback")
	requestToken, err := connector.RequestToken(redirectURL)
	if err != nil {
		logger.Error("[Pocket:Authorize] %v", err)


@@ 61,7 62,7 @@ func (h *handler) pocketCallback(w http.ResponseWriter, r *http.Request) {
		return
	}

	connector := pocket.NewConnector(h.cfg.PocketConsumerKey(integration.PocketConsumerKey))
	connector := pocket.NewConnector(config.Opts.PocketConsumerKey(integration.PocketConsumerKey))
	accessToken, err := connector.AccessToken(request.PocketRequestToken(r))
	if err != nil {
		logger.Error("[Pocket:Callback] %v", err)

M ui/integration_show.go => ui/integration_show.go +2 -1
@@ 7,6 7,7 @@ package ui // import "miniflux.app/ui"
import (
	"net/http"

	"miniflux.app/config"
	"miniflux.app/http/request"
	"miniflux.app/http/response/html"
	"miniflux.app/ui/form"


@@ 59,7 60,7 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
	view.Set("user", user)
	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
	view.Set("hasPocketConsumerKeyConfigured", h.cfg.PocketConsumerKey("") != "")
	view.Set("hasPocketConsumerKeyConfigured", config.Opts.PocketConsumerKey("") != "")

	html.OK(w, r, view.Render("integrations"))
}

M ui/login_check.go => ui/login_check.go +3 -2
@@ 3,6 3,7 @@ package ui  // import "miniflux.app/ui"
import (
	"net/http"

	"miniflux.app/config"
	"miniflux.app/http/cookie"
	"miniflux.app/http/request"
	"miniflux.app/http/response/html"


@@ 55,8 56,8 @@ func (h *handler) checkLogin(w http.ResponseWriter, r *http.Request) {
	http.SetCookie(w, cookie.New(
		cookie.CookieUserSessionID,
		sessionToken,
		h.cfg.IsHTTPS,
		h.cfg.BasePath(),
		config.Opts.HTTPS,
		config.Opts.BasePath(),
	))

	html.Redirect(w, r, route.Path(h.router, "unread"))

M ui/logout.go => ui/logout.go +3 -2
@@ 7,6 7,7 @@ package ui // import "miniflux.app/ui"
import (
	"net/http"

	"miniflux.app/config"
	"miniflux.app/http/cookie"
	"miniflux.app/http/request"
	"miniflux.app/http/response/html"


@@ 32,8 33,8 @@ func (h *handler) logout(w http.ResponseWriter, r *http.Request) {

	http.SetCookie(w, cookie.Expired(
		cookie.CookieUserSessionID,
		h.cfg.IsHTTPS,
		h.cfg.BasePath(),
		config.Opts.HTTPS,
		config.Opts.BasePath(),
	))

	html.Redirect(w, r, route.Path(h.router, "login"))

M ui/middleware.go => ui/middleware.go +6 -7
@@ 14,21 14,20 @@ import (
	"miniflux.app/http/request"
	"miniflux.app/http/response/html"
	"miniflux.app/http/route"
	"miniflux.app/storage"
	"miniflux.app/logger"
	"miniflux.app/model"
	"miniflux.app/storage"

	"github.com/gorilla/mux"
)

type middleware struct {
	router *mux.Router
	cfg *config.Config
	store *storage.Storage
	store  *storage.Storage
}

func newMiddleware(router *mux.Router, cfg *config.Config, store *storage.Storage) *middleware {
	return &middleware{router, cfg, store}
func newMiddleware(router *mux.Router, store *storage.Storage) *middleware {
	return &middleware{router, store}
}

func (m *middleware) handleUserSession(next http.Handler) http.Handler {


@@ 61,7 60,7 @@ func (m *middleware) handleAppSession(next http.Handler) http.Handler {
		session := m.getAppSessionValueFromCookie(r)

		if session == nil {
			if (request.IsAuthenticated(r)) {
			if request.IsAuthenticated(r) {
				userID := request.UserID(r)
				logger.Debug("[UI:AppSession] Cookie expired but user #%d is logged: creating a new session", userID)
				session, err = m.store.CreateAppSessionWithUserPrefs(userID)


@@ 78,7 77,7 @@ func (m *middleware) handleAppSession(next http.Handler) http.Handler {
				}
			}

			http.SetCookie(w, cookie.New(cookie.CookieAppSessionID, session.ID, m.cfg.IsHTTPS, m.cfg.BasePath()))
			http.SetCookie(w, cookie.New(cookie.CookieAppSessionID, session.ID, config.Opts.HTTPS, config.Opts.BasePath()))
		} else {
			logger.Debug("[UI:AppSession] %s", session)
		}

M ui/oauth2.go => ui/oauth2.go +4 -4
@@ 9,10 9,10 @@ import (
	"miniflux.app/oauth2"
)

func getOAuth2Manager(cfg *config.Config) *oauth2.Manager {
func getOAuth2Manager() *oauth2.Manager {
	return oauth2.NewManager(
		cfg.OAuth2ClientID(),
		cfg.OAuth2ClientSecret(),
		cfg.OAuth2RedirectURL(),
		config.Opts.OAuth2ClientID(),
		config.Opts.OAuth2ClientSecret(),
		config.Opts.OAuth2RedirectURL(),
	)
}

M ui/oauth2_callback.go => ui/oauth2_callback.go +5 -4
@@ 7,6 7,7 @@ package ui // import "miniflux.app/ui"
import (
	"net/http"

	"miniflux.app/config"
	"miniflux.app/http/cookie"
	"miniflux.app/http/request"
	"miniflux.app/http/response/html"


@@ 43,7 44,7 @@ func (h *handler) oauth2Callback(w http.ResponseWriter, r *http.Request) {
		return
	}

	authProvider, err := getOAuth2Manager(h.cfg).Provider(provider)
	authProvider, err := getOAuth2Manager().Provider(provider)
	if err != nil {
		logger.Error("[OAuth2] %v", err)
		html.Redirect(w, r, route.Path(h.router, "login"))


@@ 90,7 91,7 @@ func (h *handler) oauth2Callback(w http.ResponseWriter, r *http.Request) {
	}

	if user == nil {
		if !h.cfg.IsOAuth2UserCreationAllowed() {
		if !config.Opts.IsOAuth2UserCreationAllowed() {
			html.Forbidden(w, r)
			return
		}


@@ 121,8 122,8 @@ func (h *handler) oauth2Callback(w http.ResponseWriter, r *http.Request) {
	http.SetCookie(w, cookie.New(
		cookie.CookieUserSessionID,
		sessionToken,
		h.cfg.IsHTTPS,
		h.cfg.BasePath(),
		config.Opts.HTTPS,
		config.Opts.BasePath(),
	))

	html.Redirect(w, r, route.Path(h.router, "unread"))

M ui/oauth2_redirect.go => ui/oauth2_redirect.go +1 -1
@@ 24,7 24,7 @@ func (h *handler) oauth2Redirect(w http.ResponseWriter, r *http.Request) {
		return
	}

	authProvider, err := getOAuth2Manager(h.cfg).Provider(provider)
	authProvider, err := getOAuth2Manager().Provider(provider)
	if err != nil {
		logger.Error("[OAuth2] %v", err)
		html.Redirect(w, r, route.Path(h.router, "login"))

M ui/oauth2_unlink.go => ui/oauth2_unlink.go +1 -1
@@ 24,7 24,7 @@ func (h *handler) oauth2Unlink(w http.ResponseWriter, r *http.Request) {
		return
	}

	authProvider, err := getOAuth2Manager(h.cfg).Provider(provider)
	authProvider, err := getOAuth2Manager().Provider(provider)
	if err != nil {
		logger.Error("[OAuth2] %v", err)
		html.Redirect(w, r, route.Path(h.router, "settings"))

M ui/ui.go => ui/ui.go +3 -4
@@ 7,7 7,6 @@ package ui // import "miniflux.app/ui"
import (
	"net/http"

	"miniflux.app/config"
	"miniflux.app/reader/feed"
	"miniflux.app/storage"
	"miniflux.app/template"


@@ 17,9 16,9 @@ import (
)

// Serve declares all routes for the user interface.
func Serve(router *mux.Router, cfg *config.Config, store *storage.Storage, pool *worker.Pool, feedHandler *feed.Handler) {
	middleware := newMiddleware(router, cfg, store)
	handler := &handler{router, cfg, store, template.NewEngine(cfg, router), pool, feedHandler}
func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool, feedHandler *feed.Handler) {
	middleware := newMiddleware(router, store)
	handler := &handler{router, store, template.NewEngine(router), pool, feedHandler}

	uiRouter := router.NewRoute().Subrouter()
	uiRouter.Use(middleware.handleUserSession)