and path/'
6 |
7 |
--------------------------------------------------------------------------------
/contrib/thunder_client/README.md:
--------------------------------------------------------------------------------
1 | Miniflux API Collection for Thunder Client VS Code Extension
2 | ============================================================
3 |
4 | Official website: https://www.thunderclient.com
5 |
6 | This folder contains the API endpoints collection for Miniflux. You can import it locally to interact with the Miniflux API.
7 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module miniflux.app/v2
2 |
3 | // +heroku goVersion go1.23
4 |
5 | require (
6 | github.com/PuerkitoBio/goquery v1.10.3
7 | github.com/andybalholm/brotli v1.1.1
8 | github.com/coreos/go-oidc/v3 v3.14.1
9 | github.com/go-webauthn/webauthn v0.13.0
10 | github.com/gorilla/mux v1.8.1
11 | github.com/lib/pq v1.10.9
12 | github.com/mattn/go-sqlite3 v1.14.28
13 | github.com/prometheus/client_golang v1.22.0
14 | github.com/tdewolff/minify/v2 v2.23.8
15 | golang.org/x/crypto v0.38.0
16 | golang.org/x/image v0.27.0
17 | golang.org/x/net v0.40.0
18 | golang.org/x/oauth2 v0.30.0
19 | golang.org/x/term v0.32.0
20 | )
21 |
22 | require (
23 | github.com/go-webauthn/x v0.1.21 // indirect
24 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
25 | github.com/google/go-tpm v0.9.5 // indirect
26 | )
27 |
28 | require (
29 | github.com/andybalholm/cascadia v1.3.3 // indirect
30 | github.com/beorn7/perks v1.0.1 // indirect
31 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
32 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect
33 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect
34 | github.com/google/uuid v1.6.0 // indirect
35 | github.com/mitchellh/mapstructure v1.5.0 // indirect
36 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
37 | github.com/prometheus/client_model v0.6.1 // indirect
38 | github.com/prometheus/common v0.62.0 // indirect
39 | github.com/prometheus/procfs v0.15.1 // indirect
40 | github.com/tdewolff/parse/v2 v2.8.1 // indirect
41 | github.com/x448/float16 v0.8.4 // indirect
42 | golang.org/x/sys v0.33.0 // indirect
43 | golang.org/x/text v0.25.0 // indirect
44 | google.golang.org/protobuf v1.36.5 // indirect
45 | )
46 |
47 | go 1.23.0
48 |
49 | toolchain go1.24.1
50 |
--------------------------------------------------------------------------------
/internal/api/icon.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package api // import "miniflux.app/v2/internal/api"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/json"
11 | )
12 |
13 | func (h *handler) getIconByFeedID(w http.ResponseWriter, r *http.Request) {
14 | feedID := request.RouteInt64Param(r, "feedID")
15 |
16 | if !h.store.HasFeedIcon(feedID) {
17 | json.NotFound(w, r)
18 | return
19 | }
20 |
21 | icon, err := h.store.IconByFeedID(request.UserID(r), feedID)
22 | if err != nil {
23 | json.ServerError(w, r, err)
24 | return
25 | }
26 |
27 | if icon == nil {
28 | json.NotFound(w, r)
29 | return
30 | }
31 |
32 | json.OK(w, r, &feedIconResponse{
33 | ID: icon.ID,
34 | MimeType: icon.MimeType,
35 | Data: icon.DataURL(),
36 | })
37 | }
38 |
39 | func (h *handler) getIconByIconID(w http.ResponseWriter, r *http.Request) {
40 | iconID := request.RouteInt64Param(r, "iconID")
41 |
42 | icon, err := h.store.IconByID(iconID)
43 | if err != nil {
44 | json.ServerError(w, r, err)
45 | return
46 | }
47 |
48 | if icon == nil {
49 | json.NotFound(w, r)
50 | return
51 | }
52 |
53 | json.OK(w, r, &feedIconResponse{
54 | ID: icon.ID,
55 | MimeType: icon.MimeType,
56 | Data: icon.DataURL(),
57 | })
58 | }
59 |
--------------------------------------------------------------------------------
/internal/api/opml.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package api // import "miniflux.app/v2/internal/api"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/json"
11 | "miniflux.app/v2/internal/http/response/xml"
12 | "miniflux.app/v2/internal/reader/opml"
13 | )
14 |
15 | func (h *handler) exportFeeds(w http.ResponseWriter, r *http.Request) {
16 | opmlHandler := opml.NewHandler(h.store)
17 | opmlExport, err := opmlHandler.Export(request.UserID(r))
18 | if err != nil {
19 | json.ServerError(w, r, err)
20 | return
21 | }
22 |
23 | xml.OK(w, r, opmlExport)
24 | }
25 |
26 | func (h *handler) importFeeds(w http.ResponseWriter, r *http.Request) {
27 | opmlHandler := opml.NewHandler(h.store)
28 | err := opmlHandler.Import(request.UserID(r), r.Body)
29 | defer r.Body.Close()
30 | if err != nil {
31 | json.ServerError(w, r, err)
32 | return
33 | }
34 |
35 | json.Created(w, r, map[string]string{"message": "Feeds imported successfully"})
36 | }
37 |
--------------------------------------------------------------------------------
/internal/api/payload.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package api // import "miniflux.app/v2/internal/api"
5 |
6 | import (
7 | "miniflux.app/v2/internal/model"
8 | )
9 |
10 | type feedIconResponse struct {
11 | ID int64 `json:"id"`
12 | MimeType string `json:"mime_type"`
13 | Data string `json:"data"`
14 | }
15 |
16 | type entriesResponse struct {
17 | Total int `json:"total"`
18 | Entries model.Entries `json:"entries"`
19 | }
20 |
21 | type feedCreationResponse struct {
22 | FeedID int64 `json:"feed_id"`
23 | }
24 |
25 | type versionResponse struct {
26 | Version string `json:"version"`
27 | Commit string `json:"commit"`
28 | BuildDate string `json:"build_date"`
29 | GoVersion string `json:"go_version"`
30 | Compiler string `json:"compiler"`
31 | Arch string `json:"arch"`
32 | OS string `json:"os"`
33 | }
34 |
--------------------------------------------------------------------------------
/internal/cli/ask_credentials.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package cli // import "miniflux.app/v2/internal/cli"
5 |
6 | import (
7 | "bufio"
8 | "fmt"
9 | "os"
10 | "strings"
11 |
12 | "golang.org/x/term"
13 | )
14 |
15 | func askCredentials() (string, string) {
16 | fd := int(os.Stdin.Fd())
17 |
18 | if !term.IsTerminal(fd) {
19 | printErrorAndExit(fmt.Errorf("this is not an interactive terminal, exiting"))
20 | }
21 |
22 | fmt.Print("Enter Username: ")
23 |
24 | reader := bufio.NewReader(os.Stdin)
25 | username, _ := reader.ReadString('\n')
26 |
27 | fmt.Print("Enter Password: ")
28 |
29 | state, _ := term.GetState(fd)
30 | defer term.Restore(fd, state)
31 | bytePassword, _ := term.ReadPassword(fd)
32 |
33 | fmt.Printf("\n")
34 | return strings.TrimSpace(username), strings.TrimSpace(string(bytePassword))
35 | }
36 |
--------------------------------------------------------------------------------
/internal/cli/create_admin.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package cli // import "miniflux.app/v2/internal/cli"
5 |
6 | import (
7 | "log/slog"
8 |
9 | "miniflux.app/v2/internal/config"
10 | "miniflux.app/v2/internal/model"
11 | "miniflux.app/v2/internal/storage"
12 | "miniflux.app/v2/internal/validator"
13 | )
14 |
15 | func createAdminUserFromEnvironmentVariables(store *storage.Storage) {
16 | createAdminUser(store, config.Opts.AdminUsername(), config.Opts.AdminPassword())
17 | }
18 |
19 | func createAdminUserFromInteractiveTerminal(store *storage.Storage) {
20 | username, password := askCredentials()
21 | createAdminUser(store, username, password)
22 | }
23 |
24 | func createAdminUser(store *storage.Storage, username, password string) {
25 | userCreationRequest := &model.UserCreationRequest{
26 | Username: username,
27 | Password: password,
28 | IsAdmin: true,
29 | }
30 |
31 | if store.UserExists(userCreationRequest.Username) {
32 | slog.Info("Skipping admin user creation because it already exists",
33 | slog.String("username", userCreationRequest.Username),
34 | )
35 | return
36 | }
37 |
38 | if validationErr := validator.ValidateUserCreationWithPassword(store, userCreationRequest); validationErr != nil {
39 | printErrorAndExit(validationErr.Error())
40 | }
41 |
42 | if user, err := store.CreateUser(userCreationRequest); err != nil {
43 | printErrorAndExit(err)
44 | } else {
45 | slog.Info("Created new admin user",
46 | slog.String("username", user.Username),
47 | slog.Int64("user_id", user.ID),
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/internal/cli/export_feeds.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package cli // import "miniflux.app/v2/internal/cli"
5 |
6 | import (
7 | "fmt"
8 |
9 | "miniflux.app/v2/internal/reader/opml"
10 | "miniflux.app/v2/internal/storage"
11 | )
12 |
13 | func exportUserFeeds(store *storage.Storage, username string) {
14 | user, err := store.UserByUsername(username)
15 | if err != nil {
16 | printErrorAndExit(fmt.Errorf("unable to find user: %w", err))
17 | }
18 |
19 | if user == nil {
20 | printErrorAndExit(fmt.Errorf("user %q not found", username))
21 | }
22 |
23 | opmlHandler := opml.NewHandler(store)
24 | opmlExport, err := opmlHandler.Export(user.ID)
25 | if err != nil {
26 | printErrorAndExit(fmt.Errorf("unable to export feeds: %w", err))
27 | }
28 |
29 | fmt.Println(opmlExport)
30 | }
31 |
--------------------------------------------------------------------------------
/internal/cli/flush_sessions.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package cli // import "miniflux.app/v2/internal/cli"
5 |
6 | import (
7 | "fmt"
8 |
9 | "miniflux.app/v2/internal/storage"
10 | )
11 |
12 | func flushSessions(store *storage.Storage) {
13 | fmt.Println("Flushing all sessions (disconnect users)")
14 | if err := store.FlushAllSessions(); err != nil {
15 | printErrorAndExit(err)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/internal/cli/health_check.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package cli // import "miniflux.app/v2/internal/cli"
5 |
6 | import (
7 | "fmt"
8 | "log/slog"
9 | "net/http"
10 | "time"
11 |
12 | "miniflux.app/v2/internal/config"
13 | )
14 |
15 | func doHealthCheck(healthCheckEndpoint string) {
16 | if healthCheckEndpoint == "auto" {
17 | healthCheckEndpoint = "http://" + config.Opts.ListenAddr() + config.Opts.BasePath() + "/healthcheck"
18 | }
19 |
20 | slog.Debug("Executing health check request", slog.String("endpoint", healthCheckEndpoint))
21 |
22 | client := &http.Client{Timeout: 3 * time.Second}
23 | resp, err := client.Get(healthCheckEndpoint)
24 | if err != nil {
25 | printErrorAndExit(fmt.Errorf(`health check failure: %v`, err))
26 | }
27 | defer resp.Body.Close()
28 |
29 | if resp.StatusCode != 200 {
30 | printErrorAndExit(fmt.Errorf(`health check failed with status code %d`, resp.StatusCode))
31 | }
32 |
33 | slog.Debug(`Health check is passing`)
34 | }
35 |
--------------------------------------------------------------------------------
/internal/cli/info.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package cli // import "miniflux.app/v2/internal/cli"
5 |
6 | import (
7 | "fmt"
8 | "runtime"
9 |
10 | "miniflux.app/v2/internal/version"
11 | )
12 |
13 | func info() {
14 | fmt.Println("Version:", version.Version)
15 | fmt.Println("Commit:", version.Commit)
16 | fmt.Println("Build Date:", version.BuildDate)
17 | fmt.Println("Go Version:", runtime.Version())
18 | fmt.Println("Compiler:", runtime.Compiler)
19 | fmt.Println("Arch:", runtime.GOARCH)
20 | fmt.Println("OS:", runtime.GOOS)
21 | }
22 |
--------------------------------------------------------------------------------
/internal/cli/logger.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package cli // import "miniflux.app/v2/internal/cli"
5 |
6 | import (
7 | "io"
8 | "log/slog"
9 | )
10 |
11 | func InitializeDefaultLogger(logLevel string, logFile io.Writer, logFormat string, logTime bool) error {
12 | var programLogLevel = new(slog.LevelVar)
13 | switch logLevel {
14 | case "debug":
15 | programLogLevel.Set(slog.LevelDebug)
16 | case "info":
17 | programLogLevel.Set(slog.LevelInfo)
18 | case "warning":
19 | programLogLevel.Set(slog.LevelWarn)
20 | case "error":
21 | programLogLevel.Set(slog.LevelError)
22 | }
23 |
24 | logHandlerOptions := &slog.HandlerOptions{Level: programLogLevel}
25 | if !logTime {
26 | logHandlerOptions.ReplaceAttr = func(groups []string, a slog.Attr) slog.Attr {
27 | if a.Key == slog.TimeKey {
28 | return slog.Attr{}
29 | }
30 |
31 | return a
32 | }
33 | }
34 |
35 | var logger *slog.Logger
36 | switch logFormat {
37 | case "json":
38 | logger = slog.New(slog.NewJSONHandler(logFile, logHandlerOptions))
39 | default:
40 | logger = slog.New(slog.NewTextHandler(logFile, logHandlerOptions))
41 | }
42 |
43 | slog.SetDefault(logger)
44 |
45 | return nil
46 | }
47 |
--------------------------------------------------------------------------------
/internal/cli/reset_password.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package cli // import "miniflux.app/v2/internal/cli"
5 |
6 | import (
7 | "fmt"
8 |
9 | "miniflux.app/v2/internal/model"
10 | "miniflux.app/v2/internal/storage"
11 | "miniflux.app/v2/internal/validator"
12 | )
13 |
14 | func resetPassword(store *storage.Storage) {
15 | username, password := askCredentials()
16 | user, err := store.UserByUsername(username)
17 | if err != nil {
18 | printErrorAndExit(err)
19 | }
20 |
21 | if user == nil {
22 | printErrorAndExit(fmt.Errorf("user not found"))
23 | }
24 |
25 | userModificationRequest := &model.UserModificationRequest{
26 | Password: &password,
27 | }
28 | if validationErr := validator.ValidateUserModification(store, user.ID, userModificationRequest); validationErr != nil {
29 | printErrorAndExit(validationErr.Error())
30 | }
31 |
32 | user.Password = password
33 | if err := store.UpdateUser(user); err != nil {
34 | printErrorAndExit(err)
35 | }
36 |
37 | fmt.Println("Password changed!")
38 | }
39 |
--------------------------------------------------------------------------------
/internal/cli/scheduler.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package cli // import "miniflux.app/v2/internal/cli"
5 |
6 | import (
7 | "log/slog"
8 | "time"
9 |
10 | "miniflux.app/v2/internal/config"
11 | "miniflux.app/v2/internal/storage"
12 | "miniflux.app/v2/internal/worker"
13 | )
14 |
15 | func runScheduler(store *storage.Storage, pool *worker.Pool) {
16 | slog.Debug(`Starting background scheduler...`)
17 |
18 | go feedScheduler(
19 | store,
20 | pool,
21 | config.Opts.PollingFrequency(),
22 | config.Opts.BatchSize(),
23 | config.Opts.PollingParsingErrorLimit(),
24 | )
25 |
26 | go cleanupScheduler(
27 | store,
28 | config.Opts.CleanupFrequencyHours(),
29 | )
30 | }
31 |
32 | func feedScheduler(store *storage.Storage, pool *worker.Pool, frequency, batchSize, errorLimit int) {
33 | for range time.Tick(time.Duration(frequency) * time.Minute) {
34 | // Generate a batch of feeds for any user that has feeds to refresh.
35 | batchBuilder := store.NewBatchBuilder()
36 | batchBuilder.WithBatchSize(batchSize)
37 | batchBuilder.WithErrorLimit(errorLimit)
38 | batchBuilder.WithoutDisabledFeeds()
39 | batchBuilder.WithNextCheckExpired()
40 |
41 | if jobs, err := batchBuilder.FetchJobs(); err != nil {
42 | slog.Error("Unable to fetch jobs from database", slog.Any("error", err))
43 | } else if len(jobs) > 0 {
44 | slog.Info("Created a batch of feeds",
45 | slog.Int("nb_jobs", len(jobs)),
46 | )
47 | pool.Push(jobs)
48 | }
49 | }
50 | }
51 |
52 | func cleanupScheduler(store *storage.Storage, frequency int) {
53 | for range time.Tick(time.Duration(frequency) * time.Hour) {
54 | runCleanupTasks(store)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package config // import "miniflux.app/v2/internal/config"
5 |
6 | // Opts holds parsed configuration options.
7 | var Opts *Options
8 |
--------------------------------------------------------------------------------
/internal/config/parser_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package config // import "miniflux.app/v2/internal/config"
5 |
6 | import (
7 | "testing"
8 | )
9 |
10 | func TestParseBoolValue(t *testing.T) {
11 | scenarios := map[string]bool{
12 | "": true,
13 | "1": true,
14 | "Yes": true,
15 | "yes": true,
16 | "True": true,
17 | "true": true,
18 | "on": true,
19 | "false": false,
20 | "off": false,
21 | "invalid": false,
22 | }
23 |
24 | for input, expected := range scenarios {
25 | result := parseBool(input, true)
26 | if result != expected {
27 | t.Errorf(`Unexpected result for %q, got %v instead of %v`, input, result, expected)
28 | }
29 | }
30 | }
31 |
32 | func TestParseStringValueWithUnsetVariable(t *testing.T) {
33 | if parseString("", "defaultValue") != "defaultValue" {
34 | t.Errorf(`Unset variables should returns the default value`)
35 | }
36 | }
37 |
38 | func TestParseStringValue(t *testing.T) {
39 | if parseString("test", "defaultValue") != "test" {
40 | t.Errorf(`Defined variables should returns the specified value`)
41 | }
42 | }
43 |
44 | func TestParseIntValueWithUnsetVariable(t *testing.T) {
45 | if parseInt("", 42) != 42 {
46 | t.Errorf(`Unset variables should returns the default value`)
47 | }
48 | }
49 |
50 | func TestParseIntValueWithInvalidInput(t *testing.T) {
51 | if parseInt("invalid integer", 42) != 42 {
52 | t.Errorf(`Invalid integer should returns the default value`)
53 | }
54 | }
55 |
56 | func TestParseIntValue(t *testing.T) {
57 | if parseInt("2018", 42) != 2018 {
58 | t.Errorf(`Defined variables should returns the specified value`)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/internal/database/postgresql.go:
--------------------------------------------------------------------------------
1 | //go:build !sqlite
2 |
3 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
4 | // SPDX-License-Identifier: Apache-2.0
5 |
6 | package database // import "miniflux.app/v2/internal/database"
7 |
8 | import (
9 | "database/sql"
10 | "time"
11 |
12 | _ "github.com/lib/pq"
13 | )
14 |
15 | // NewConnectionPool configures the database connection pool.
16 | func NewConnectionPool(dsn string, minConnections, maxConnections int, connectionLifetime time.Duration) (*sql.DB, error) {
17 | db, err := sql.Open("postgres", dsn)
18 | if err != nil {
19 | return nil, err
20 | }
21 |
22 | db.SetMaxOpenConns(maxConnections)
23 | db.SetMaxIdleConns(minConnections)
24 | db.SetConnMaxLifetime(connectionLifetime)
25 |
26 | return db, nil
27 | }
28 |
29 | func getDriverStr() string {
30 | return "postgresql"
31 | }
32 |
--------------------------------------------------------------------------------
/internal/database/sqlite.go:
--------------------------------------------------------------------------------
1 | //go:build sqlite
2 |
3 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
4 | // SPDX-License-Identifier: Apache-2.0
5 |
6 | package database // import "miniflux.app/v2/internal/database"
7 |
8 | import (
9 | "database/sql"
10 | "time"
11 |
12 | _ "github.com/mattn/go-sqlite3"
13 | )
14 |
15 | // NewConnectionPool configures the database connection pool.
16 | func NewConnectionPool(dsn string, _, _ int, _ time.Duration) (*sql.DB, error) {
17 | db, err := sql.Open("sqlite3", dsn)
18 | if err != nil {
19 | return nil, err
20 | }
21 | return db, nil
22 | }
23 |
24 | func getDriverStr() string {
25 | return "sqlite3"
26 | }
27 |
--------------------------------------------------------------------------------
/internal/googlereader/prefix_suffix.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package googlereader // import "miniflux.app/v2/internal/googlereader"
5 |
6 | const (
7 | // StreamPrefix is the prefix for astreams (read/starred/reading list and so on)
8 | StreamPrefix = "user/-/state/com.google/"
9 | // UserStreamPrefix is the user specific prefix for streams (read/starred/reading list and so on)
10 | UserStreamPrefix = "user/%d/state/com.google/"
11 | // LabelPrefix is the prefix for a label stream
12 | LabelPrefix = "user/-/label/"
13 | // UserLabelPrefix is the user specific prefix prefix for a label stream
14 | UserLabelPrefix = "user/%d/label/"
15 | // FeedPrefix is the prefix for a feed stream
16 | FeedPrefix = "feed/"
17 | // Read is the suffix for read stream
18 | Read = "read"
19 | // Starred is the suffix for starred stream
20 | Starred = "starred"
21 | // ReadingList is the suffix for reading list stream
22 | ReadingList = "reading-list"
23 | // KeptUnread is the suffix for kept unread stream
24 | KeptUnread = "kept-unread"
25 | // Broadcast is the suffix for broadcast stream
26 | Broadcast = "broadcast"
27 | // BroadcastFriends is the suffix for broadcast friends stream
28 | BroadcastFriends = "broadcast-friends"
29 | // Like is the suffix for like stream
30 | Like = "like"
31 | )
32 |
--------------------------------------------------------------------------------
/internal/http/cookie/cookie.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package cookie // import "miniflux.app/v2/internal/http/cookie"
5 |
6 | import (
7 | "net/http"
8 | "time"
9 |
10 | "miniflux.app/v2/internal/config"
11 | )
12 |
13 | // Cookie names.
14 | const (
15 | CookieAppSessionID = "MinifluxAppSessionID"
16 | CookieUserSessionID = "MinifluxUserSessionID"
17 | )
18 |
19 | // New creates a new cookie.
20 | func New(name, value string, isHTTPS bool, path string) *http.Cookie {
21 | return &http.Cookie{
22 | Name: name,
23 | Value: value,
24 | Path: basePath(path),
25 | Secure: isHTTPS,
26 | HttpOnly: true,
27 | Expires: time.Now().Add(time.Duration(config.Opts.CleanupRemoveSessionsDays()) * 24 * time.Hour),
28 | SameSite: http.SameSiteLaxMode,
29 | }
30 | }
31 |
32 | // Expired returns an expired cookie.
33 | func Expired(name string, isHTTPS bool, path string) *http.Cookie {
34 | return &http.Cookie{
35 | Name: name,
36 | Value: "",
37 | Path: basePath(path),
38 | Secure: isHTTPS,
39 | HttpOnly: true,
40 | MaxAge: -1,
41 | Expires: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
42 | SameSite: http.SameSiteLaxMode,
43 | }
44 | }
45 |
46 | func basePath(path string) string {
47 | if path == "" {
48 | return "/"
49 | }
50 | return path
51 | }
52 |
--------------------------------------------------------------------------------
/internal/http/request/client_ip.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package request // import "miniflux.app/v2/internal/http/request"
5 |
6 | import (
7 | "net"
8 | "net/http"
9 | "strings"
10 | )
11 |
12 | // FindClientIP returns the client real IP address based on trusted Reverse-Proxy HTTP headers.
13 | func FindClientIP(r *http.Request) string {
14 | headers := []string{"X-Forwarded-For", "X-Real-Ip"}
15 | for _, header := range headers {
16 | value := r.Header.Get(header)
17 |
18 | if value != "" {
19 | addresses := strings.Split(value, ",")
20 | address := strings.TrimSpace(addresses[0])
21 | address = dropIPv6zone(address)
22 |
23 | if net.ParseIP(address) != nil {
24 | return address
25 | }
26 | }
27 | }
28 |
29 | // Fallback to TCP/IP source IP address.
30 | return FindRemoteIP(r)
31 | }
32 |
33 | // FindRemoteIP returns remote client IP address without considering HTTP headers.
34 | func FindRemoteIP(r *http.Request) string {
35 | remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
36 | if err != nil {
37 | remoteIP = r.RemoteAddr
38 | }
39 | return dropIPv6zone(remoteIP)
40 | }
41 |
42 | func dropIPv6zone(address string) string {
43 | i := strings.IndexByte(address, '%')
44 | if i != -1 {
45 | address = address[:i]
46 | }
47 | return address
48 | }
49 |
--------------------------------------------------------------------------------
/internal/http/request/cookie.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package request // import "miniflux.app/v2/internal/http/request"
5 |
6 | import "net/http"
7 |
8 | // CookieValue returns the cookie value.
9 | func CookieValue(r *http.Request, name string) string {
10 | cookie, err := r.Cookie(name)
11 | if err != nil {
12 | return ""
13 | }
14 |
15 | return cookie.Value
16 | }
17 |
--------------------------------------------------------------------------------
/internal/http/request/cookie_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package request // import "miniflux.app/v2/internal/http/request"
5 |
6 | import (
7 | "net/http"
8 | "testing"
9 | )
10 |
11 | func TestGetCookieValue(t *testing.T) {
12 | r, _ := http.NewRequest("GET", "http://example.org", nil)
13 | r.AddCookie(&http.Cookie{Value: "cookie_value", Name: "my_cookie"})
14 |
15 | result := CookieValue(r, "my_cookie")
16 | expected := "cookie_value"
17 |
18 | if result != expected {
19 | t.Errorf(`Unexpected cookie value, got %q instead of %q`, result, expected)
20 | }
21 | }
22 |
23 | func TestGetCookieValueWhenUnset(t *testing.T) {
24 | r, _ := http.NewRequest("GET", "http://example.org", nil)
25 |
26 | result := CookieValue(r, "my_cookie")
27 | expected := ""
28 |
29 | if result != expected {
30 | t.Errorf(`Unexpected cookie value, got %q instead of %q`, result, expected)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/internal/http/response/response.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package response // import "miniflux.app/v2/internal/http/response"
5 |
6 | // ContentSecurityPolicyForUntrustedContent is the default CSP for untrusted content.
7 | // default-src 'none' disables all content sources
8 | // form-action 'none' disables all form submissions
9 | // sandbox enables a sandbox for the requested resource
10 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
11 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/form-action
12 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox
13 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src
14 | const ContentSecurityPolicyForUntrustedContent = `default-src 'none'; form-action 'none'; sandbox;`
15 |
--------------------------------------------------------------------------------
/internal/http/response/xml/xml.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package xml // import "miniflux.app/v2/internal/http/response/xml"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/response"
10 | )
11 |
12 | // OK writes a standard XML response with a status 200 OK.
13 | func OK(w http.ResponseWriter, r *http.Request, body interface{}) {
14 | builder := response.New(w, r)
15 | builder.WithHeader("Content-Type", "text/xml; charset=utf-8")
16 | builder.WithBody(body)
17 | builder.Write()
18 | }
19 |
20 | // Attachment forces the XML document to be downloaded by the web browser.
21 | func Attachment(w http.ResponseWriter, r *http.Request, filename string, body interface{}) {
22 | builder := response.New(w, r)
23 | builder.WithHeader("Content-Type", "text/xml; charset=utf-8")
24 | builder.WithAttachment(filename)
25 | builder.WithBody(body)
26 | builder.Write()
27 | }
28 |
--------------------------------------------------------------------------------
/internal/http/route/route.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package route // import "miniflux.app/v2/internal/http/route"
5 |
6 | import (
7 | "strconv"
8 |
9 | "github.com/gorilla/mux"
10 | )
11 |
12 | // Path returns the defined route based on given arguments.
13 | func Path(router *mux.Router, name string, args ...any) string {
14 | route := router.Get(name)
15 | if route == nil {
16 | panic("route not found: " + name)
17 | }
18 |
19 | var pairs []string
20 | for _, arg := range args {
21 | switch param := arg.(type) {
22 | case string:
23 | pairs = append(pairs, param)
24 | case int64:
25 | pairs = append(pairs, strconv.FormatInt(param, 10))
26 | }
27 | }
28 |
29 | result, err := route.URLPath(pairs...)
30 | if err != nil {
31 | panic(err)
32 | }
33 |
34 | return result.String()
35 | }
36 |
--------------------------------------------------------------------------------
/internal/http/server/middleware.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package httpd // import "miniflux.app/v2/internal/http/server"
5 |
6 | import (
7 | "context"
8 | "log/slog"
9 | "net/http"
10 | "time"
11 |
12 | "miniflux.app/v2/internal/config"
13 | "miniflux.app/v2/internal/http/request"
14 | )
15 |
16 | func middleware(next http.Handler) http.Handler {
17 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18 | clientIP := request.FindClientIP(r)
19 | ctx := r.Context()
20 | ctx = context.WithValue(ctx, request.ClientIPContextKey, clientIP)
21 |
22 | if r.Header.Get("X-Forwarded-Proto") == "https" {
23 | config.Opts.HTTPS = true
24 | }
25 |
26 | t1 := time.Now()
27 | defer func() {
28 | slog.Debug("Incoming request",
29 | slog.String("client_ip", clientIP),
30 | slog.Group("request",
31 | slog.String("method", r.Method),
32 | slog.String("uri", r.RequestURI),
33 | slog.String("protocol", r.Proto),
34 | slog.Duration("execution_time", time.Since(t1)),
35 | ),
36 | )
37 | }()
38 |
39 | if config.Opts.HTTPS && config.Opts.HasHSTS() {
40 | w.Header().Set("Strict-Transport-Security", "max-age=31536000")
41 | }
42 |
43 | next.ServeHTTP(w, r.WithContext(ctx))
44 | })
45 | }
46 |
--------------------------------------------------------------------------------
/internal/integration/betula/betula.go:
--------------------------------------------------------------------------------
1 | package betula
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/url"
7 | "strings"
8 | "time"
9 |
10 | "miniflux.app/v2/internal/urllib"
11 | "miniflux.app/v2/internal/version"
12 | )
13 |
14 | const defaultClientTimeout = 10 * time.Second
15 |
16 | type Client struct {
17 | url string
18 | token string
19 | }
20 |
21 | func NewClient(url, token string) *Client {
22 | return &Client{url: url, token: token}
23 | }
24 |
25 | func (c *Client) CreateBookmark(entryURL, entryTitle string, tags []string) error {
26 | apiEndpoint, err := urllib.JoinBaseURLAndPath(c.url, "/save-link")
27 | if err != nil {
28 | return fmt.Errorf("betula: unable to generate save-link endpoint: %v", err)
29 | }
30 |
31 | values := url.Values{}
32 | values.Add("url", entryURL)
33 | values.Add("title", entryTitle)
34 | values.Add("tags", strings.Join(tags, ","))
35 |
36 | request, err := http.NewRequest(http.MethodPost, apiEndpoint+"?"+values.Encode(), nil)
37 | if err != nil {
38 | return fmt.Errorf("betula: unable to create request: %v", err)
39 | }
40 |
41 | request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
42 | request.Header.Set("User-Agent", "Miniflux/"+version.Version)
43 | request.AddCookie(&http.Cookie{Name: "betula-token", Value: c.token})
44 |
45 | httpClient := &http.Client{Timeout: defaultClientTimeout}
46 | response, err := httpClient.Do(request)
47 | if err != nil {
48 | return fmt.Errorf("betula: unable to send request: %v", err)
49 | }
50 | defer response.Body.Close()
51 |
52 | if response.StatusCode >= 400 {
53 | return fmt.Errorf("betula: unable to create bookmark: url=%s status=%d", apiEndpoint, response.StatusCode)
54 | }
55 |
56 | return nil
57 | }
58 |
--------------------------------------------------------------------------------
/internal/integration/matrixbot/matrixbot.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package matrixbot // import "miniflux.app/v2/internal/integration/matrixbot"
5 |
6 | import (
7 | "fmt"
8 | "strings"
9 |
10 | "miniflux.app/v2/internal/model"
11 | )
12 |
13 | // PushEntries pushes entries to matrix chat using integration settings provided
14 | func PushEntries(feed *model.Feed, entries model.Entries, matrixBaseURL, matrixUsername, matrixPassword, matrixRoomID string) error {
15 | client := NewClient(matrixBaseURL)
16 | discovery, err := client.DiscoverEndpoints()
17 | if err != nil {
18 | return err
19 | }
20 |
21 | loginResponse, err := client.Login(discovery.HomeServerInformation.BaseURL, matrixUsername, matrixPassword)
22 | if err != nil {
23 | return err
24 | }
25 |
26 | var textMessages []string
27 | var formattedTextMessages []string
28 |
29 | for _, entry := range entries {
30 | textMessages = append(textMessages, fmt.Sprintf(`[%s] %s - %s`, feed.Title, entry.Title, entry.URL))
31 | formattedTextMessages = append(formattedTextMessages, fmt.Sprintf(`%s: %s`, feed.Title, entry.URL, entry.Title))
32 | }
33 |
34 | _, err = client.SendFormattedTextMessage(
35 | discovery.HomeServerInformation.BaseURL,
36 | loginResponse.AccessToken,
37 | matrixRoomID,
38 | strings.Join(textMessages, "\n"),
39 | ""+strings.Join(formattedTextMessages, "\n")+"
",
40 | )
41 |
42 | return err
43 | }
44 |
--------------------------------------------------------------------------------
/internal/locale/error.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package locale // import "miniflux.app/v2/internal/locale"
5 |
6 | import "errors"
7 |
8 | type LocalizedErrorWrapper struct {
9 | originalErr error
10 | translationKey string
11 | translationArgs []any
12 | }
13 |
14 | func NewLocalizedErrorWrapper(originalErr error, translationKey string, translationArgs ...any) *LocalizedErrorWrapper {
15 | return &LocalizedErrorWrapper{
16 | originalErr: originalErr,
17 | translationKey: translationKey,
18 | translationArgs: translationArgs,
19 | }
20 | }
21 |
22 | func (l *LocalizedErrorWrapper) Error() error {
23 | return l.originalErr
24 | }
25 |
26 | func (l *LocalizedErrorWrapper) Translate(language string) string {
27 | if l.translationKey == "" {
28 | return l.originalErr.Error()
29 | }
30 | return NewPrinter(language).Printf(l.translationKey, l.translationArgs...)
31 | }
32 |
33 | type LocalizedError struct {
34 | translationKey string
35 | translationArgs []any
36 | }
37 |
38 | func NewLocalizedError(translationKey string, translationArgs ...any) *LocalizedError {
39 | return &LocalizedError{translationKey: translationKey, translationArgs: translationArgs}
40 | }
41 |
42 | func (v *LocalizedError) String() string {
43 | return NewPrinter("en_US").Printf(v.translationKey, v.translationArgs...)
44 | }
45 |
46 | func (v *LocalizedError) Error() error {
47 | return errors.New(v.String())
48 | }
49 |
50 | func (v *LocalizedError) Translate(language string) string {
51 | return NewPrinter(language).Printf(v.translationKey, v.translationArgs...)
52 | }
53 |
--------------------------------------------------------------------------------
/internal/locale/locale.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package locale // import "miniflux.app/v2/internal/locale"
5 |
6 | // AvailableLanguages is the list of available languages.
7 | var AvailableLanguages = map[string]string{
8 | "de_DE": "Deutsch",
9 | "el_EL": "Ελληνικά",
10 | "en_US": "English",
11 | "es_ES": "Español",
12 | "fi_FI": "Suomi",
13 | "fr_FR": "Français",
14 | "hi_IN": "हिन्दी",
15 | "id_ID": "Bahasa Indonesia",
16 | "it_IT": "Italiano",
17 | "ja_JP": "日本語",
18 | "nan_Latn_pehoeji": "Pe̍h-ōe-jī",
19 | "nl_NL": "Nederlands",
20 | "pl_PL": "Polski",
21 | "pt_BR": "Português Brasileiro",
22 | "ro_RO": "Română",
23 | "ru_RU": "Русский",
24 | "tr_TR": "Türkçe",
25 | "uk_UA": "Українська",
26 | "zh_CN": "简体中文",
27 | "zh_TW": "繁體中文",
28 | }
29 |
--------------------------------------------------------------------------------
/internal/locale/locale_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package locale // import "miniflux.app/v2/internal/locale"
5 |
6 | import "testing"
7 |
8 | func TestAvailableLanguages(t *testing.T) {
9 | results := AvailableLanguages
10 | for k, v := range results {
11 | if k == "" {
12 | t.Errorf(`Empty language key detected`)
13 | }
14 |
15 | if v == "" {
16 | t.Errorf(`Empty language value detected`)
17 | }
18 | }
19 |
20 | if _, found := results["en_US"]; !found {
21 | t.Errorf(`We must have at least the default language (en_US)`)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/internal/model/api_key.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package model // import "miniflux.app/v2/internal/model"
5 |
6 | import (
7 | "time"
8 | )
9 |
10 | // APIKey represents an application API key.
11 | type APIKey struct {
12 | ID int64 `json:"id"`
13 | UserID int64 `json:"user_id"`
14 | Token string `json:"token"`
15 | Description string `json:"description"`
16 | LastUsedAt *time.Time `json:"last_used_at"`
17 | CreatedAt time.Time `json:"created_at"`
18 | }
19 |
20 | // APIKeys represents a collection of API Key.
21 | type APIKeys []*APIKey
22 |
23 | // APIKeyCreationRequest represents the request to create a new API Key.
24 | type APIKeyCreationRequest struct {
25 | Description string `json:"description"`
26 | }
27 |
--------------------------------------------------------------------------------
/internal/model/categories_sort_options.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package model // import "miniflux.app/v2/internal/model"
5 |
6 | func CategoriesSortingOptions() map[string]string {
7 | return map[string]string{
8 | "unread_count": "form.prefs.select.unread_count",
9 | "alphabetical": "form.prefs.select.alphabetical",
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/internal/model/category.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package model // import "miniflux.app/v2/internal/model"
5 |
6 | import "fmt"
7 |
8 | // Category represents a feed category.
9 | type Category struct {
10 | ID int64 `json:"id"`
11 | Title string `json:"title"`
12 | UserID int64 `json:"user_id"`
13 | HideGlobally bool `json:"hide_globally"`
14 | FeedCount *int `json:"feed_count,omitempty"`
15 | TotalUnread *int `json:"total_unread,omitempty"`
16 | }
17 |
18 | func (c *Category) String() string {
19 | return fmt.Sprintf("ID=%d, UserID=%d, Title=%s", c.ID, c.UserID, c.Title)
20 | }
21 |
22 | type CategoryCreationRequest struct {
23 | Title string `json:"title"`
24 | HideGlobally bool `json:"hide_globally"`
25 | }
26 |
27 | type CategoryModificationRequest struct {
28 | Title *string `json:"title"`
29 | HideGlobally *bool `json:"hide_globally"`
30 | }
31 |
32 | func (c *CategoryModificationRequest) Patch(category *Category) {
33 | if c.Title != nil {
34 | category.Title = *c.Title
35 | }
36 |
37 | if c.HideGlobally != nil {
38 | category.HideGlobally = *c.HideGlobally
39 | }
40 | }
41 |
42 | // Categories represents a list of categories.
43 | type Categories []*Category
44 |
--------------------------------------------------------------------------------
/internal/model/enclosure_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package model
5 |
6 | import (
7 | "testing"
8 | )
9 |
10 | func TestEnclosure_Html5MimeTypeGivesOriginalMimeType(t *testing.T) {
11 | enclosure := Enclosure{MimeType: "thing/thisMimeTypeIsNotExpectedToBeReplaced"}
12 | if enclosure.Html5MimeType() != enclosure.MimeType {
13 | t.Fatalf(
14 | "HTML5 MimeType must provide original MimeType if not explicitly Replaced. Got %s ,expected '%s' ",
15 | enclosure.Html5MimeType(),
16 | enclosure.MimeType,
17 | )
18 | }
19 | }
20 |
21 | func TestEnclosure_Html5MimeTypeReplaceStandardM4vByAppleSpecificMimeType(t *testing.T) {
22 | enclosure := Enclosure{MimeType: "video/m4v"}
23 | if enclosure.Html5MimeType() != "video/x-m4v" {
24 | // Solution from this stackoverflow discussion:
25 | // https://stackoverflow.com/questions/15277147/m4v-mimetype-video-mp4-or-video-m4v/66945470#66945470
26 | // tested at the time of this commit (06/2023) on latest Firefox & Vivaldi on this feed
27 | // https://www.florenceporcel.com/podcast/lfhdu.xml
28 | t.Fatalf(
29 | "HTML5 MimeType must be replaced by 'video/x-m4v' when originally video/m4v to ensure playbacks in brownser. Got '%s'",
30 | enclosure.Html5MimeType(),
31 | )
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/internal/model/home_page.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package model // import "miniflux.app/v2/internal/model"
5 |
6 | // HomePages returns the list of available home pages.
7 | func HomePages() map[string]string {
8 | return map[string]string{
9 | "unread": "menu.unread",
10 | "starred": "menu.starred",
11 | "history": "menu.history",
12 | "feeds": "menu.feeds",
13 | "categories": "menu.categories",
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/internal/model/icon.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package model // import "miniflux.app/v2/internal/model"
5 |
6 | import (
7 | "encoding/base64"
8 | "fmt"
9 | )
10 |
11 | // Icon represents a website icon (favicon)
12 | type Icon struct {
13 | ID int64 `json:"id"`
14 | Hash string `json:"hash"`
15 | MimeType string `json:"mime_type"`
16 | Content []byte `json:"-"`
17 | ExternalID string `json:"external_id"`
18 | }
19 |
20 | // DataURL returns the data URL of the icon.
21 | func (i *Icon) DataURL() string {
22 | return fmt.Sprintf("%s;base64,%s", i.MimeType, base64.StdEncoding.EncodeToString(i.Content))
23 | }
24 |
25 | // Icons represents a list of icons.
26 | type Icons []*Icon
27 |
28 | // FeedIcon is a junction table between feeds and icons.
29 | type FeedIcon struct {
30 | FeedID int64 `json:"feed_id"`
31 | IconID int64 `json:"icon_id"`
32 | ExternalIconID string `json:"external_icon_id"`
33 | }
34 |
--------------------------------------------------------------------------------
/internal/model/job.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package model // import "miniflux.app/v2/internal/model"
5 |
6 | // Job represents a payload sent to the processing queue.
7 | type Job struct {
8 | UserID int64
9 | FeedID int64
10 | }
11 |
12 | // JobList represents a list of jobs.
13 | type JobList []Job
14 |
--------------------------------------------------------------------------------
/internal/model/model.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package model // import "miniflux.app/v2/internal/model"
5 |
6 | type Number interface {
7 | int | int64 | float64
8 | }
9 |
10 | func OptionalNumber[T Number](value T) *T {
11 | if value > 0 {
12 | return &value
13 | }
14 | return nil
15 | }
16 |
17 | func OptionalString(value string) *string {
18 | if value != "" {
19 | return &value
20 | }
21 | return nil
22 | }
23 |
24 | func SetOptionalField[T any](value T) *T {
25 | return &value
26 | }
27 |
--------------------------------------------------------------------------------
/internal/model/subscription.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package model // import "miniflux.app/v2/internal/model"
5 |
6 | // SubscriptionDiscoveryRequest represents a request to discover subscriptions.
7 | type SubscriptionDiscoveryRequest struct {
8 | URL string `json:"url"`
9 | UserAgent string `json:"user_agent"`
10 | Cookie string `json:"cookie"`
11 | Username string `json:"username"`
12 | Password string `json:"password"`
13 | ProxyURL string `json:"proxy_url"`
14 | FetchViaProxy bool `json:"fetch_via_proxy"`
15 | AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
16 | DisableHTTP2 bool `json:"disable_http2"`
17 | }
18 |
--------------------------------------------------------------------------------
/internal/model/theme.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package model // import "miniflux.app/v2/internal/model"
5 |
6 | // Themes returns the list of available themes.
7 | func Themes() map[string]string {
8 | return map[string]string{
9 | "light_serif": "Light - Serif",
10 | "light_sans_serif": "Light - Sans Serif",
11 | "dark_serif": "Dark - Serif",
12 | "dark_sans_serif": "Dark - Sans Serif",
13 | "system_serif": "System - Serif",
14 | "system_sans_serif": "System - Sans Serif",
15 | }
16 | }
17 |
18 | // ThemeColor returns the color for the address bar or/and the browser color.
19 | // https://developer.mozilla.org/en-US/docs/Web/Manifest#theme_color
20 | // https://developers.google.com/web/tools/lighthouse/audits/address-bar
21 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name/theme-color
22 | func ThemeColor(theme, colorScheme string) string {
23 | switch theme {
24 | case "dark_serif", "dark_sans_serif":
25 | return "#222"
26 | case "system_serif", "system_sans_serif":
27 | if colorScheme == "dark" {
28 | return "#222"
29 | }
30 |
31 | return "#fff"
32 | default:
33 | return "#fff"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/internal/model/user_session.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package model // import "miniflux.app/v2/internal/model"
5 |
6 | import (
7 | "fmt"
8 | "time"
9 |
10 | "miniflux.app/v2/internal/timezone"
11 | )
12 |
13 | // UserSession represents a user session in the system.
14 | type UserSession struct {
15 | ID int64
16 | UserID int64
17 | Token string
18 | CreatedAt time.Time
19 | UserAgent string
20 | IP string
21 | }
22 |
23 | func (u *UserSession) String() string {
24 | return fmt.Sprintf(`ID=%q, UserID=%q, IP=%q, Token=%q`, u.ID, u.UserID, u.IP, u.Token)
25 | }
26 |
27 | // UseTimezone converts creation date to the given timezone.
28 | func (u *UserSession) UseTimezone(tz string) {
29 | u.CreatedAt = timezone.Convert(tz, u.CreatedAt)
30 | }
31 |
32 | // UserSessions represents a list of sessions.
33 | type UserSessions []*UserSession
34 |
35 | // UseTimezone converts creation date of all sessions to the given timezone.
36 | func (u UserSessions) UseTimezone(tz string) {
37 | for _, session := range u {
38 | session.UseTimezone(tz)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/internal/model/webauthn.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package model // import "miniflux.app/v2/internal/model"
5 |
6 | import (
7 | "database/sql/driver"
8 | "encoding/hex"
9 | "encoding/json"
10 | "errors"
11 | "fmt"
12 | "time"
13 |
14 | "github.com/go-webauthn/webauthn/webauthn"
15 | )
16 |
17 | // handle marshalling / unmarshalling session data
18 | type WebAuthnSession struct {
19 | *webauthn.SessionData
20 | }
21 |
22 | func (s WebAuthnSession) Value() (driver.Value, error) {
23 | return json.Marshal(s)
24 | }
25 |
26 | func (s *WebAuthnSession) Scan(value interface{}) error {
27 | b, ok := value.([]byte)
28 | if !ok {
29 | return errors.New("type assertion to []byte failed")
30 | }
31 |
32 | return json.Unmarshal(b, &s)
33 | }
34 |
35 | func (s WebAuthnSession) String() string {
36 | if s.SessionData == nil {
37 | return "{}"
38 | }
39 | return fmt.Sprintf("{Challenge: %s, UserID: %x}", s.Challenge, s.UserID)
40 | }
41 |
42 | type WebAuthnCredential struct {
43 | Credential webauthn.Credential
44 | Name string
45 | AddedOn *time.Time
46 | LastSeenOn *time.Time
47 | Handle []byte
48 | }
49 |
50 | func (s WebAuthnCredential) HandleEncoded() string {
51 | return hex.EncodeToString(s.Handle)
52 | }
53 |
--------------------------------------------------------------------------------
/internal/oauth2/authorization.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package oauth2 // import "miniflux.app/v2/internal/oauth2"
5 |
6 | import (
7 | "crypto/sha256"
8 | "encoding/base64"
9 |
10 | "golang.org/x/oauth2"
11 |
12 | "miniflux.app/v2/internal/crypto"
13 | )
14 |
15 | type Authorization struct {
16 | url string
17 | state string
18 | codeVerifier string
19 | }
20 |
21 | func (u *Authorization) RedirectURL() string {
22 | return u.url
23 | }
24 |
25 | func (u *Authorization) State() string {
26 | return u.state
27 | }
28 |
29 | func (u *Authorization) CodeVerifier() string {
30 | return u.codeVerifier
31 | }
32 |
33 | func GenerateAuthorization(config *oauth2.Config) *Authorization {
34 | codeVerifier := crypto.GenerateRandomStringHex(32)
35 | sum := sha256.Sum256([]byte(codeVerifier))
36 |
37 | state := crypto.GenerateRandomStringHex(24)
38 |
39 | authUrl := config.AuthCodeURL(
40 | state,
41 | oauth2.SetAuthURLParam("code_challenge_method", "S256"),
42 | oauth2.SetAuthURLParam("code_challenge", base64.RawURLEncoding.EncodeToString(sum[:])),
43 | )
44 |
45 | return &Authorization{
46 | url: authUrl,
47 | state: state,
48 | codeVerifier: codeVerifier,
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/internal/oauth2/manager.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package oauth2 // import "miniflux.app/v2/internal/oauth2"
5 |
6 | import (
7 | "context"
8 | "errors"
9 | "log/slog"
10 | )
11 |
12 | type Manager struct {
13 | providers map[string]Provider
14 | }
15 |
16 | func (m *Manager) FindProvider(name string) (Provider, error) {
17 | if provider, found := m.providers[name]; found {
18 | return provider, nil
19 | }
20 |
21 | return nil, errors.New("oauth2 provider not found")
22 | }
23 |
24 | func (m *Manager) AddProvider(name string, provider Provider) {
25 | m.providers[name] = provider
26 | }
27 |
28 | func NewManager(ctx context.Context, clientID, clientSecret, redirectURL, oidcDiscoveryEndpoint string) *Manager {
29 | m := &Manager{providers: make(map[string]Provider)}
30 | m.AddProvider("google", NewGoogleProvider(clientID, clientSecret, redirectURL))
31 |
32 | if oidcDiscoveryEndpoint != "" {
33 | if genericOidcProvider, err := NewOidcProvider(ctx, clientID, clientSecret, redirectURL, oidcDiscoveryEndpoint); err != nil {
34 | slog.Error("Failed to initialize OIDC provider",
35 | slog.Any("error", err),
36 | )
37 | } else {
38 | m.AddProvider("oidc", genericOidcProvider)
39 | }
40 | }
41 |
42 | if clientSecret == "" {
43 | slog.Warn("OIDC client secret is empty or missing.")
44 | }
45 |
46 | return m
47 | }
48 |
--------------------------------------------------------------------------------
/internal/oauth2/profile.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package oauth2 // import "miniflux.app/v2/internal/oauth2"
5 |
6 | import (
7 | "fmt"
8 | )
9 |
10 | // Profile is the OAuth2 user profile.
11 | type Profile struct {
12 | Key string
13 | ID string
14 | Username string
15 | }
16 |
17 | func (p Profile) String() string {
18 | return fmt.Sprintf(`Key=%s ; ID=%s ; Username=%s`, p.Key, p.ID, p.Username)
19 | }
20 |
--------------------------------------------------------------------------------
/internal/oauth2/provider.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package oauth2 // import "miniflux.app/v2/internal/oauth2"
5 |
6 | import (
7 | "context"
8 |
9 | "golang.org/x/oauth2"
10 |
11 | "miniflux.app/v2/internal/model"
12 | )
13 |
14 | // Provider is an interface for OAuth2 providers.
15 | type Provider interface {
16 | GetConfig() *oauth2.Config
17 | GetUserExtraKey() string
18 | GetProfile(ctx context.Context, code, codeVerifier string) (*Profile, error)
19 | PopulateUserCreationWithProfileID(user *model.UserCreationRequest, profile *Profile)
20 | PopulateUserWithProfileID(user *model.User, profile *Profile)
21 | UnsetUserProfileID(user *model.User)
22 | }
23 |
--------------------------------------------------------------------------------
/internal/proxyrotator/proxyrotator.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package proxyrotator // import "miniflux.app/v2/internal/proxyrotator"
5 |
6 | import (
7 | "net/url"
8 | "sync"
9 | )
10 |
11 | var ProxyRotatorInstance *ProxyRotator
12 |
13 | // ProxyRotator manages a list of proxies and rotates through them.
14 | type ProxyRotator struct {
15 | proxies []*url.URL
16 | currentIndex int
17 | mutex sync.Mutex
18 | }
19 |
20 | // NewProxyRotator creates a new ProxyRotator with the given proxy URLs.
21 | func NewProxyRotator(proxyURLs []string) (*ProxyRotator, error) {
22 | parsedProxies := make([]*url.URL, 0, len(proxyURLs))
23 |
24 | for _, p := range proxyURLs {
25 | proxyURL, err := url.Parse(p)
26 | if err != nil {
27 | return nil, err
28 | }
29 | parsedProxies = append(parsedProxies, proxyURL)
30 | }
31 |
32 | return &ProxyRotator{
33 | proxies: parsedProxies,
34 | currentIndex: 0,
35 | mutex: sync.Mutex{},
36 | }, nil
37 | }
38 |
39 | // GetNextProxy returns the next proxy in the rotation.
40 | func (pr *ProxyRotator) GetNextProxy() *url.URL {
41 | pr.mutex.Lock()
42 | defer pr.mutex.Unlock()
43 |
44 | if len(pr.proxies) == 0 {
45 | return nil
46 | }
47 |
48 | proxy := pr.proxies[pr.currentIndex]
49 | pr.currentIndex = (pr.currentIndex + 1) % len(pr.proxies)
50 |
51 | return proxy
52 | }
53 |
54 | // HasProxies checks if there are any proxies available in the rotator.
55 | func (pr *ProxyRotator) HasProxies() bool {
56 | pr.mutex.Lock()
57 | defer pr.mutex.Unlock()
58 |
59 | return len(pr.proxies) > 0
60 | }
61 |
--------------------------------------------------------------------------------
/internal/reader/atom/parser.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package atom // import "miniflux.app/v2/internal/reader/atom"
5 |
6 | import (
7 | "fmt"
8 | "io"
9 |
10 | "miniflux.app/v2/internal/model"
11 | xml_decoder "miniflux.app/v2/internal/reader/xml"
12 | )
13 |
14 | // Parse returns a normalized feed struct from a Atom feed.
15 | func Parse(baseURL string, r io.ReadSeeker, version string) (*model.Feed, error) {
16 | switch version {
17 | case "0.3":
18 | atomFeed := new(Atom03Feed)
19 | if err := xml_decoder.NewXMLDecoder(r).Decode(atomFeed); err != nil {
20 | return nil, fmt.Errorf("atom: unable to parse Atom 0.3 feed: %w", err)
21 | }
22 | return NewAtom03Adapter(atomFeed).BuildFeed(baseURL), nil
23 | default:
24 | atomFeed := new(Atom10Feed)
25 | if err := xml_decoder.NewXMLDecoder(r).Decode(atomFeed); err != nil {
26 | return nil, fmt.Errorf("atom: unable to parse Atom 1.0 feed: %w", err)
27 | }
28 | return NewAtom10Adapter(atomFeed).BuildFeed(baseURL), nil
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/internal/reader/dublincore/dublincore.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package dublincore // import "miniflux.app/v2/internal/reader/dublincore"
5 |
6 | type DublinCoreChannelElement struct {
7 | DublinCoreCreator string `xml:"http://purl.org/dc/elements/1.1/ creator"`
8 | }
9 |
10 | type DublinCoreItemElement struct {
11 | DublinCoreTitle string `xml:"http://purl.org/dc/elements/1.1/ title"`
12 | DublinCoreDate string `xml:"http://purl.org/dc/elements/1.1/ date"`
13 | DublinCoreCreator string `xml:"http://purl.org/dc/elements/1.1/ creator"`
14 | DublinCoreContent string `xml:"http://purl.org/rss/1.0/modules/content/ encoded"`
15 | }
16 |
--------------------------------------------------------------------------------
/internal/reader/encoding/testdata/invalid-prolog.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 테스트 피드
4 |
5 | Café
6 |
7 |
--------------------------------------------------------------------------------
/internal/reader/encoding/testdata/iso-8859-1-meta-after-1024.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/reader/encoding/testdata/iso-8859-1-meta-after-1024.html
--------------------------------------------------------------------------------
/internal/reader/encoding/testdata/iso-8859-1.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/reader/encoding/testdata/iso-8859-1.html
--------------------------------------------------------------------------------
/internal/reader/encoding/testdata/iso-8859-1.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/reader/encoding/testdata/iso-8859-1.xml
--------------------------------------------------------------------------------
/internal/reader/encoding/testdata/utf8-incorrect-prolog.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 테스트 피드
4 |
5 | Café
6 |
7 |
--------------------------------------------------------------------------------
/internal/reader/encoding/testdata/utf8-meta-after-1024.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
42 |
43 | Frédéric
44 |
45 |
46 | Café
47 |
48 |
--------------------------------------------------------------------------------
/internal/reader/encoding/testdata/utf8.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Café
6 |
7 |
8 | Café
9 |
10 |
--------------------------------------------------------------------------------
/internal/reader/encoding/testdata/utf8.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 테스트 피드
4 |
5 | Café
6 |
7 |
--------------------------------------------------------------------------------
/internal/reader/encoding/testdata/windows-1252-incorrect-prolog.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Euro €
5 |
6 |
--------------------------------------------------------------------------------
/internal/reader/encoding/testdata/windows-1252.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/reader/encoding/testdata/windows-1252.xml
--------------------------------------------------------------------------------
/internal/reader/fetcher/encoding_wrappers.go:
--------------------------------------------------------------------------------
1 | package fetcher
2 |
3 | import (
4 | "compress/gzip"
5 | "io"
6 |
7 | "github.com/andybalholm/brotli"
8 | )
9 |
10 | type brotliReadCloser struct {
11 | body io.ReadCloser
12 | brotliReader io.Reader
13 | }
14 |
15 | func NewBrotliReadCloser(body io.ReadCloser) *brotliReadCloser {
16 | return &brotliReadCloser{
17 | body: body,
18 | brotliReader: brotli.NewReader(body),
19 | }
20 | }
21 |
22 | func (b *brotliReadCloser) Read(p []byte) (n int, err error) {
23 | return b.brotliReader.Read(p)
24 | }
25 |
26 | func (b *brotliReadCloser) Close() error {
27 | return b.body.Close()
28 | }
29 |
30 | type gzipReadCloser struct {
31 | body io.ReadCloser
32 | gzipReader io.Reader
33 | gzipErr error
34 | }
35 |
36 | func NewGzipReadCloser(body io.ReadCloser) *gzipReadCloser {
37 | return &gzipReadCloser{body: body}
38 | }
39 |
40 | func (gz *gzipReadCloser) Read(p []byte) (n int, err error) {
41 | if gz.gzipReader == nil {
42 | if gz.gzipErr == nil {
43 | gz.gzipReader, gz.gzipErr = gzip.NewReader(gz.body)
44 | }
45 | if gz.gzipErr != nil {
46 | return 0, gz.gzipErr
47 | }
48 | }
49 |
50 | return gz.gzipReader.Read(p)
51 | }
52 |
53 | func (gz *gzipReadCloser) Close() error {
54 | return gz.body.Close()
55 | }
56 |
--------------------------------------------------------------------------------
/internal/reader/googleplay/googleplay.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package googleplay // import "miniflux.app/v2/internal/reader/googleplay"
5 |
6 | // Specs:
7 | // https://support.google.com/googleplay/podcasts/answer/6260341
8 | // https://www.google.com/schemas/play-podcasts/1.0/play-podcasts.xsd
9 | type GooglePlayChannelElement struct {
10 | GooglePlayAuthor string `xml:"http://www.google.com/schemas/play-podcasts/1.0 author"`
11 | GooglePlayEmail string `xml:"http://www.google.com/schemas/play-podcasts/1.0 email"`
12 | GooglePlayImage GooglePlayImageElement `xml:"http://www.google.com/schemas/play-podcasts/1.0 image"`
13 | GooglePlayDescription string `xml:"http://www.google.com/schemas/play-podcasts/1.0 description"`
14 | GooglePlayCategory GooglePlayCategoryElement `xml:"http://www.google.com/schemas/play-podcasts/1.0 category"`
15 | }
16 |
17 | type GooglePlayItemElement struct {
18 | GooglePlayAuthor string `xml:"http://www.google.com/schemas/play-podcasts/1.0 author"`
19 | GooglePlayDescription string `xml:"http://www.google.com/schemas/play-podcasts/1.0 description"`
20 | GooglePlayExplicit string `xml:"http://www.google.com/schemas/play-podcasts/1.0 explicit"`
21 | GooglePlayBlock string `xml:"http://www.google.com/schemas/play-podcasts/1.0 block"`
22 | GooglePlayNewFeedURL string `xml:"http://www.google.com/schemas/play-podcasts/1.0 new-feed-url"`
23 | }
24 |
25 | type GooglePlayImageElement struct {
26 | Href string `xml:"href,attr"`
27 | }
28 |
29 | type GooglePlayCategoryElement struct {
30 | Text string `xml:"text,attr"`
31 | }
32 |
--------------------------------------------------------------------------------
/internal/reader/json/parser.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package json // import "miniflux.app/v2/internal/reader/json"
5 |
6 | import (
7 | "encoding/json"
8 | "fmt"
9 | "io"
10 |
11 | "miniflux.app/v2/internal/model"
12 | )
13 |
14 | // Parse returns a normalized feed struct from a JSON feed.
15 | func Parse(baseURL string, data io.Reader) (*model.Feed, error) {
16 | jsonFeed := new(JSONFeed)
17 | if err := json.NewDecoder(data).Decode(&jsonFeed); err != nil {
18 | return nil, fmt.Errorf("json: unable to parse feed: %w", err)
19 | }
20 |
21 | return NewJSONAdapter(jsonFeed).BuildFeed(baseURL), nil
22 | }
23 |
--------------------------------------------------------------------------------
/internal/reader/opml/parser.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package opml // import "miniflux.app/v2/internal/reader/opml"
5 |
6 | import (
7 | "encoding/xml"
8 | "fmt"
9 | "io"
10 |
11 | "miniflux.app/v2/internal/reader/encoding"
12 | )
13 |
14 | // Parse reads an OPML file and returns a SubcriptionList.
15 | func Parse(data io.Reader) (SubcriptionList, error) {
16 | opmlDocument := NewOPMLDocument()
17 | decoder := xml.NewDecoder(data)
18 | decoder.Entity = xml.HTMLEntity
19 | decoder.Strict = false
20 | decoder.CharsetReader = encoding.CharsetReader
21 |
22 | err := decoder.Decode(opmlDocument)
23 | if err != nil {
24 | return nil, fmt.Errorf("opml: unable to parse document: %w", err)
25 | }
26 |
27 | return getSubscriptionsFromOutlines(opmlDocument.Outlines, ""), nil
28 | }
29 |
30 | func getSubscriptionsFromOutlines(outlines opmlOutlineCollection, category string) (subscriptions SubcriptionList) {
31 | for _, outline := range outlines {
32 | if outline.IsSubscription() {
33 | subscriptions = append(subscriptions, &Subcription{
34 | Title: outline.GetTitle(),
35 | FeedURL: outline.FeedURL,
36 | SiteURL: outline.GetSiteURL(),
37 | Description: outline.Description,
38 | CategoryName: category,
39 | })
40 | } else if outline.Outlines.HasChildren() {
41 | subscriptions = append(subscriptions, getSubscriptionsFromOutlines(outline.Outlines, outline.GetTitle())...)
42 | }
43 | }
44 | return subscriptions
45 | }
46 |
--------------------------------------------------------------------------------
/internal/reader/opml/subscription.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package opml // import "miniflux.app/v2/internal/reader/opml"
5 |
6 | // Subcription represents a feed that will be imported or exported.
7 | type Subcription struct {
8 | Title string
9 | SiteURL string
10 | FeedURL string
11 | CategoryName string
12 | Description string
13 | }
14 |
15 | // Equals compare two subscriptions.
16 | func (s Subcription) Equals(subscription *Subcription) bool {
17 | return s.Title == subscription.Title && s.SiteURL == subscription.SiteURL &&
18 | s.FeedURL == subscription.FeedURL && s.CategoryName == subscription.CategoryName &&
19 | s.Description == subscription.Description
20 | }
21 |
22 | // SubcriptionList is a list of subscriptions.
23 | type SubcriptionList []*Subcription
24 |
--------------------------------------------------------------------------------
/internal/reader/parser/format.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package parser // import "miniflux.app/v2/internal/reader/parser"
5 |
6 | import (
7 | "bytes"
8 | "encoding/xml"
9 | "io"
10 |
11 | rxml "miniflux.app/v2/internal/reader/xml"
12 | )
13 |
14 | // List of feed formats.
15 | const (
16 | FormatRDF = "rdf"
17 | FormatRSS = "rss"
18 | FormatAtom = "atom"
19 | FormatJSON = "json"
20 | FormatUnknown = "unknown"
21 | )
22 |
23 | // DetectFeedFormat tries to guess the feed format from input data.
24 | func DetectFeedFormat(r io.ReadSeeker) (string, string) {
25 | data := make([]byte, 512)
26 | r.Read(data)
27 |
28 | if bytes.HasPrefix(bytes.TrimSpace(data), []byte("{")) {
29 | return FormatJSON, ""
30 | }
31 |
32 | r.Seek(0, io.SeekStart)
33 | decoder := rxml.NewXMLDecoder(r)
34 |
35 | for {
36 | token, _ := decoder.Token()
37 | if token == nil {
38 | break
39 | }
40 |
41 | if element, ok := token.(xml.StartElement); ok {
42 | switch element.Name.Local {
43 | case "rss":
44 | return FormatRSS, ""
45 | case "feed":
46 | for _, attr := range element.Attr {
47 | if attr.Name.Local == "version" && attr.Value == "0.3" {
48 | return FormatAtom, "0.3"
49 | }
50 | }
51 | return FormatAtom, "1.0"
52 | case "RDF":
53 | return FormatRDF, ""
54 | }
55 | }
56 | }
57 |
58 | return FormatUnknown, ""
59 | }
60 |
--------------------------------------------------------------------------------
/internal/reader/parser/parser.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package parser // import "miniflux.app/v2/internal/reader/parser"
5 |
6 | import (
7 | "errors"
8 | "io"
9 |
10 | "miniflux.app/v2/internal/model"
11 | "miniflux.app/v2/internal/reader/atom"
12 | "miniflux.app/v2/internal/reader/json"
13 | "miniflux.app/v2/internal/reader/rdf"
14 | "miniflux.app/v2/internal/reader/rss"
15 | )
16 |
17 | var ErrFeedFormatNotDetected = errors.New("parser: unable to detect feed format")
18 |
19 | // ParseFeed analyzes the input data and returns a normalized feed object.
20 | func ParseFeed(baseURL string, r io.ReadSeeker) (*model.Feed, error) {
21 | r.Seek(0, io.SeekStart)
22 | format, version := DetectFeedFormat(r)
23 | switch format {
24 | case FormatAtom:
25 | r.Seek(0, io.SeekStart)
26 | return atom.Parse(baseURL, r, version)
27 | case FormatRSS:
28 | r.Seek(0, io.SeekStart)
29 | return rss.Parse(baseURL, r)
30 | case FormatJSON:
31 | r.Seek(0, io.SeekStart)
32 | return json.Parse(baseURL, r)
33 | case FormatRDF:
34 | r.Seek(0, io.SeekStart)
35 | return rdf.Parse(baseURL, r)
36 | default:
37 | return nil, ErrFeedFormatNotDetected
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/internal/reader/parser/testdata/encoding_ISO-8859-1.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/reader/parser/testdata/encoding_ISO-8859-1.xml
--------------------------------------------------------------------------------
/internal/reader/parser/testdata/encoding_WINDOWS-1251.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/reader/parser/testdata/encoding_WINDOWS-1251.xml
--------------------------------------------------------------------------------
/internal/reader/parser/testdata/no_encoding_ISO-8859-1.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/reader/parser/testdata/no_encoding_ISO-8859-1.xml
--------------------------------------------------------------------------------
/internal/reader/rdf/parser.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package rdf // import "miniflux.app/v2/internal/reader/rdf"
5 |
6 | import (
7 | "fmt"
8 | "io"
9 |
10 | "miniflux.app/v2/internal/model"
11 | "miniflux.app/v2/internal/reader/xml"
12 | )
13 |
14 | // Parse returns a normalized feed struct from a RDF feed.
15 | func Parse(baseURL string, data io.ReadSeeker) (*model.Feed, error) {
16 | xmlFeed := new(RDF)
17 | if err := xml.NewXMLDecoder(data).Decode(xmlFeed); err != nil {
18 | return nil, fmt.Errorf("rdf: unable to parse feed: %w", err)
19 | }
20 |
21 | return NewRDFAdapter(xmlFeed).BuildFeed(baseURL), nil
22 | }
23 |
--------------------------------------------------------------------------------
/internal/reader/rdf/rdf.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package rdf // import "miniflux.app/v2/internal/reader/rdf"
5 |
6 | import (
7 | "encoding/xml"
8 |
9 | "miniflux.app/v2/internal/reader/dublincore"
10 | )
11 |
12 | // RDF sepcs: https://web.resource.org/rss/1.0/spec
13 | type RDF struct {
14 | XMLName xml.Name `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# RDF"`
15 | Channel RDFChannel `xml:"channel"`
16 | Items []RDFItem `xml:"item"`
17 | }
18 |
19 | type RDFChannel struct {
20 | Title string `xml:"title"`
21 | Link string `xml:"link"`
22 | Description string `xml:"description"`
23 | dublincore.DublinCoreChannelElement
24 | }
25 |
26 | type RDFItem struct {
27 | Title string `xml:"http://purl.org/rss/1.0/ title"`
28 | Link string `xml:"link"`
29 | Description string `xml:"description"`
30 | dublincore.DublinCoreItemElement
31 | }
32 |
--------------------------------------------------------------------------------
/internal/reader/readability/testdata:
--------------------------------------------------------------------------------
1 | ../../reader/sanitizer/testdata/
--------------------------------------------------------------------------------
/internal/reader/readingtime/readingtime.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | // Package readingtime provides a function to estimate the reading time of an article.
5 | package readingtime
6 |
7 | import (
8 | "math"
9 | "strings"
10 | "unicode"
11 | "unicode/utf8"
12 |
13 | "miniflux.app/v2/internal/reader/sanitizer"
14 | )
15 |
16 | // EstimateReadingTime returns the estimated reading time of an article in minute.
17 | func EstimateReadingTime(content string, defaultReadingSpeed, cjkReadingSpeed int) int {
18 | sanitizedContent := sanitizer.StripTags(content)
19 | truncationPoint := min(len(sanitizedContent), 50)
20 |
21 | if isCJK(sanitizedContent[:truncationPoint]) {
22 | return int(math.Ceil(float64(utf8.RuneCountInString(sanitizedContent)) / float64(cjkReadingSpeed)))
23 | }
24 | return int(math.Ceil(float64(len(strings.Fields(sanitizedContent))) / float64(defaultReadingSpeed)))
25 | }
26 |
27 | func isCJK(text string) bool {
28 | totalCJK := 0
29 |
30 | for _, r := range text[:min(len(text), 50)] {
31 | if unicode.Is(unicode.Han, r) ||
32 | unicode.Is(unicode.Hangul, r) ||
33 | unicode.Is(unicode.Hiragana, r) ||
34 | unicode.Is(unicode.Katakana, r) ||
35 | unicode.Is(unicode.Yi, r) ||
36 | unicode.Is(unicode.Bopomofo, r) {
37 | totalCJK++
38 | }
39 | }
40 |
41 | // if at least 50% of the text is CJK, odds are that the text is in CJK.
42 | return totalCJK > len(text)/50
43 | }
44 |
--------------------------------------------------------------------------------
/internal/reader/rss/atom.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package rss // import "miniflux.app/v2/internal/reader/rss"
5 |
6 | import (
7 | "miniflux.app/v2/internal/reader/atom"
8 | )
9 |
10 | type AtomAuthor struct {
11 | Author atom.AtomPerson `xml:"http://www.w3.org/2005/Atom author"`
12 | }
13 |
14 | func (a *AtomAuthor) PersonName() string {
15 | return a.Author.PersonName()
16 | }
17 |
18 | type AtomLinks struct {
19 | Links []*atom.AtomLink `xml:"http://www.w3.org/2005/Atom link"`
20 | }
21 |
--------------------------------------------------------------------------------
/internal/reader/rss/feedburner.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package rss // import "miniflux.app/v2/internal/reader/rss"
5 |
6 | // FeedBurnerItemElement represents FeedBurner XML elements.
7 | type FeedBurnerItemElement struct {
8 | FeedBurnerLink string `xml:"http://rssnamespace.org/feedburner/ext/1.0 origLink"`
9 | FeedBurnerEnclosureLink string `xml:"http://rssnamespace.org/feedburner/ext/1.0 origEnclosureLink"`
10 | }
11 |
--------------------------------------------------------------------------------
/internal/reader/rss/parser.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package rss // import "miniflux.app/v2/internal/reader/rss"
5 |
6 | import (
7 | "fmt"
8 | "io"
9 |
10 | "miniflux.app/v2/internal/model"
11 | "miniflux.app/v2/internal/reader/xml"
12 | )
13 |
14 | // Parse returns a normalized feed struct from a RSS feed.
15 | func Parse(baseURL string, data io.ReadSeeker) (*model.Feed, error) {
16 | rssFeed := new(RSS)
17 | decoder := xml.NewXMLDecoder(data)
18 | decoder.DefaultSpace = "rss"
19 | if err := decoder.Decode(rssFeed); err != nil {
20 | return nil, fmt.Errorf("rss: unable to parse feed: %w", err)
21 | }
22 | return NewRSSAdapter(rssFeed).BuildFeed(baseURL), nil
23 | }
24 |
--------------------------------------------------------------------------------
/internal/reader/rss/podcast.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package rss // import "miniflux.app/v2/internal/reader/rss"
5 |
6 | import (
7 | "errors"
8 | "math"
9 | "strconv"
10 | "strings"
11 | )
12 |
13 | var ErrInvalidDurationFormat = errors.New("rss: invalid duration format")
14 |
15 | func getDurationInMinutes(rawDuration string) (int, error) {
16 | var sumSeconds int
17 |
18 | durationParts := strings.Split(rawDuration, ":")
19 | if len(durationParts) > 3 {
20 | return 0, ErrInvalidDurationFormat
21 | }
22 |
23 | for i, durationPart := range durationParts {
24 | durationPartValue, err := strconv.Atoi(durationPart)
25 | if err != nil {
26 | return 0, ErrInvalidDurationFormat
27 | }
28 |
29 | sumSeconds += int(math.Pow(60, float64(len(durationParts)-i-1))) * durationPartValue
30 | }
31 |
32 | return sumSeconds / 60, nil
33 | }
34 |
--------------------------------------------------------------------------------
/internal/reader/sanitizer/strip_tags.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package sanitizer // import "miniflux.app/v2/internal/reader/sanitizer"
5 |
6 | import (
7 | "io"
8 | "strings"
9 |
10 | "golang.org/x/net/html"
11 | )
12 |
13 | // StripTags removes all HTML/XML tags from the input string.
14 | // This function must *only* be used for cosmetic purposes, not to prevent code injections like XSS.
15 | func StripTags(input string) string {
16 | tokenizer := html.NewTokenizer(strings.NewReader(input))
17 | var buffer strings.Builder
18 |
19 | for {
20 | if tokenizer.Next() == html.ErrorToken {
21 | err := tokenizer.Err()
22 | if err == io.EOF {
23 | return buffer.String()
24 | }
25 |
26 | return ""
27 | }
28 |
29 | token := tokenizer.Token()
30 | if token.Type == html.TextToken {
31 | buffer.WriteString(token.Data)
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/internal/reader/sanitizer/strip_tags_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package sanitizer // import "miniflux.app/v2/internal/reader/sanitizer"
5 |
6 | import "testing"
7 |
8 | func TestStripTags(t *testing.T) {
9 | input := `This link is relative and this image:
`
10 | expected := `This link is relative and this image: `
11 | output := StripTags(input)
12 |
13 | if expected != output {
14 | t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/internal/reader/sanitizer/truncate.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package sanitizer
5 |
6 | import "strings"
7 |
8 | func TruncateHTML(input string, max int) string {
9 | text := StripTags(input)
10 |
11 | // Collapse multiple spaces into a single space
12 | text = strings.Join(strings.Fields(text), " ")
13 |
14 | // Convert to runes to be safe with unicode
15 | runes := []rune(text)
16 | if len(runes) > max {
17 | return strings.TrimSpace(string(runes[:max])) + "…"
18 | }
19 |
20 | return text
21 | }
22 |
--------------------------------------------------------------------------------
/internal/reader/scraper/testdata/iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/internal/reader/scraper/testdata/iframe.html-result:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/internal/reader/scraper/testdata/img.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/internal/reader/scraper/testdata/img.html-result:
--------------------------------------------------------------------------------
1 | 



2 |
--------------------------------------------------------------------------------
/internal/reader/scraper/testdata/p.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Lorem ipsum dolor sit amet, consectetuer adipiscing ept.
6 | Apquam tincidunt mauris eu risus.
7 | Vestibulum auctor dapibus neque.
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/internal/reader/scraper/testdata/p.html-result:
--------------------------------------------------------------------------------
1 | Lorem ipsum dolor sit amet, consectetuer adipiscing ept.
Apquam tincidunt mauris eu risus.
Vestibulum auctor dapibus neque.
2 |
--------------------------------------------------------------------------------
/internal/reader/subscription/subscription.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package subscription // import "miniflux.app/v2/internal/reader/subscription"
5 |
6 | import "fmt"
7 |
8 | // Subscription represents a feed subscription.
9 | type Subscription struct {
10 | Title string `json:"title"`
11 | URL string `json:"url"`
12 | Type string `json:"type"`
13 | }
14 |
15 | func NewSubscription(title, url, kind string) *Subscription {
16 | return &Subscription{Title: title, URL: url, Type: kind}
17 | }
18 |
19 | func (s Subscription) String() string {
20 | return fmt.Sprintf(`Title=%q, URL=%q, Type=%q`, s.Title, s.URL, s.Type)
21 | }
22 |
23 | // Subscriptions represents a list of subscription.
24 | type Subscriptions []*Subscription
25 |
--------------------------------------------------------------------------------
/internal/storage/storage.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package storage // import "miniflux.app/v2/internal/storage"
5 |
6 | import (
7 | "context"
8 | "database/sql"
9 | "time"
10 | )
11 |
12 | // Storage handles all operations related to the database.
13 | type Storage struct {
14 | db *sql.DB
15 | }
16 |
17 | // NewStorage returns a new Storage.
18 | func NewStorage(db *sql.DB) *Storage {
19 | return &Storage{db}
20 | }
21 |
22 | // DatabaseVersion returns the version of the database which is in use.
23 | func (s *Storage) DatabaseVersion() string {
24 | var dbVersion string
25 | err := s.db.QueryRow(`SELECT current_setting('server_version')`).Scan(&dbVersion)
26 | if err != nil {
27 | return err.Error()
28 | }
29 |
30 | return dbVersion
31 | }
32 |
33 | // Ping checks if the database connection works.
34 | func (s *Storage) Ping() error {
35 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
36 | defer cancel()
37 |
38 | return s.db.PingContext(ctx)
39 | }
40 |
41 | // DBStats returns database statistics.
42 | func (s *Storage) DBStats() sql.DBStats {
43 | return s.db.Stats()
44 | }
45 |
46 | // DBSize returns how much size the database is using in a pretty way.
47 | func (s *Storage) DBSize() (string, error) {
48 | var size string
49 |
50 | err := s.db.QueryRow("SELECT pg_size_pretty(pg_database_size(current_database()))").Scan(&size)
51 | if err != nil {
52 | return "", err
53 | }
54 |
55 | return size, nil
56 | }
57 |
--------------------------------------------------------------------------------
/internal/storage/timezone.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package storage // import "miniflux.app/v2/internal/storage"
5 |
6 | import (
7 | "fmt"
8 | "strings"
9 | )
10 |
11 | // Timezones returns all timezones supported by the database.
12 | func (s *Storage) Timezones() (map[string]string, error) {
13 | timezones := make(map[string]string)
14 | rows, err := s.db.Query(`SELECT name FROM pg_timezone_names ORDER BY name ASC`)
15 | if err != nil {
16 | return nil, fmt.Errorf(`store: unable to fetch timezones: %v`, err)
17 | }
18 | defer rows.Close()
19 |
20 | for rows.Next() {
21 | var timezone string
22 | if err := rows.Scan(&timezone); err != nil {
23 | return nil, fmt.Errorf(`store: unable to fetch timezones row: %v`, err)
24 | }
25 |
26 | if !strings.HasPrefix(timezone, "posix") && !strings.HasPrefix(timezone, "SystemV") && timezone != "localtime" {
27 | timezones[timezone] = timezone
28 | }
29 | }
30 |
31 | return timezones, nil
32 | }
33 |
--------------------------------------------------------------------------------
/internal/template/templates/common/entry_pagination.html:
--------------------------------------------------------------------------------
1 | {{ define "entry_pagination" }}
2 |
19 | {{ end }}
20 |
--------------------------------------------------------------------------------
/internal/template/templates/common/feed_menu.html:
--------------------------------------------------------------------------------
1 | {{ define "feed_menu" }}
2 |
23 | {{ end }}
24 |
--------------------------------------------------------------------------------
/internal/template/templates/common/settings_menu.html:
--------------------------------------------------------------------------------
1 | {{ define "settings_menu" }}
2 |
26 | {{ end }}
27 |
--------------------------------------------------------------------------------
/internal/template/templates/standalone/offline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ t "page.offline.title" }} - Miniflux
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{ t "page.offline.message" }} - {{ t "page.offline.refresh_page" }}.
13 |
14 |
--------------------------------------------------------------------------------
/internal/template/templates/views/create_api_key.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.new_api_key.title" }}{{ end }}
2 |
3 | {{ define "page_header"}}
4 |
8 | {{ end }}
9 |
10 | {{ define "content"}}
11 |
25 | {{ end }}
26 |
--------------------------------------------------------------------------------
/internal/template/templates/views/create_category.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.new_category.title" }}{{ end }}
2 |
3 | {{ define "page_header"}}
4 |
14 | {{ end }}
15 |
16 | {{ define "content"}}
17 |
31 | {{ end }}
32 |
--------------------------------------------------------------------------------
/internal/template/templates/views/feeds.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.feeds.title" }} ({{ .total }}){{ end }}
2 |
3 | {{ define "page_header"}}
4 |
8 | {{ end }}
9 |
10 | {{ define "content"}}
11 | {{ if not .feeds }}
12 | {{ t "alert.no_feed" }}
13 | {{ else }}
14 | {{ template "feed_list" dict "user" .user "feeds" .feeds "ParsingErrorCount" .ParsingErrorCount }}
15 | {{ end }}
16 |
17 | {{ end }}
18 |
--------------------------------------------------------------------------------
/internal/template/templates/views/import.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.import.title" }}{{ end }}
2 |
3 | {{ define "page_header"}}
4 |
8 | {{ end }}
9 |
10 | {{ define "content"}}
11 | {{ if .errorMessage }}
12 | {{ .errorMessage }}
13 | {{ end }}
14 |
15 |
25 |
26 |
36 |
37 | {{ end }}
38 |
--------------------------------------------------------------------------------
/internal/template/templates/views/sessions.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.sessions.title" }}{{ end }}
2 |
3 | {{ define "page_header"}}
4 |
8 | {{ end }}
9 |
10 | {{ define "content"}}
11 |
12 |
13 | {{ t "page.sessions.table.date" }} |
14 | {{ t "page.sessions.table.ip" }} |
15 | {{ t "page.sessions.table.user_agent" }} |
16 | {{ t "page.sessions.table.actions" }} |
17 |
18 | {{ range .sessions }}
19 |
20 | {{ elapsed $.user.Timezone .CreatedAt }} |
21 | {{ .IP }} |
22 | {{ .UserAgent }} |
23 |
24 | {{ if eq .Token $.currentSessionToken }}
25 | {{ t "page.sessions.table.current_session" }}
26 | {{ else }}
27 | {{ icon "delete" }}{{ t "action.remove" }}
34 | {{ end }}
35 | |
36 |
37 | {{ end }}
38 |
39 |
40 | {{ end }}
41 |
--------------------------------------------------------------------------------
/internal/template/templates/views/webauthn_rename.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.webauthn_rename.title" }}{{ end }}
2 |
3 | {{ define "page_header"}}
4 |
7 | {{ end }}
8 |
9 | {{ define "content"}}
10 |
24 | {{ end }}
25 |
--------------------------------------------------------------------------------
/internal/timezone/timezone.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package timezone // import "miniflux.app/v2/internal/timezone"
5 |
6 | import (
7 | "time"
8 | )
9 |
10 | // Convert converts provided date time to actual timezone.
11 | func Convert(tz string, t time.Time) time.Time {
12 | userTimezone := getLocation(tz)
13 |
14 | if t.Location().String() == "" {
15 | if t.Before(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)) {
16 | return time.Date(0, time.January, 1, 0, 0, 0, 0, userTimezone)
17 | }
18 |
19 | // In this case, the provided date is already converted to the user timezone by Postgres,
20 | // but the timezone information is not set in the time struct.
21 | // We cannot use time.In() because the date will be converted a second time.
22 | return time.Date(
23 | t.Year(),
24 | t.Month(),
25 | t.Day(),
26 | t.Hour(),
27 | t.Minute(),
28 | t.Second(),
29 | t.Nanosecond(),
30 | userTimezone,
31 | )
32 | } else if t.Location() != userTimezone {
33 | return t.In(userTimezone)
34 | }
35 |
36 | return t
37 | }
38 |
39 | // Now returns the current time with the given timezone.
40 | func Now(tz string) time.Time {
41 | return time.Now().In(getLocation(tz))
42 | }
43 |
44 | func getLocation(tz string) *time.Location {
45 | loc, err := time.LoadLocation(tz)
46 | if err != nil {
47 | loc = time.Local
48 | }
49 | return loc
50 | }
51 |
--------------------------------------------------------------------------------
/internal/ui/about.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 | "runtime"
9 |
10 | "miniflux.app/v2/internal/config"
11 | "miniflux.app/v2/internal/http/request"
12 | "miniflux.app/v2/internal/http/response/html"
13 | "miniflux.app/v2/internal/ui/session"
14 | "miniflux.app/v2/internal/ui/view"
15 | "miniflux.app/v2/internal/version"
16 | )
17 |
18 | func (h *handler) showAboutPage(w http.ResponseWriter, r *http.Request) {
19 | user, err := h.store.UserByID(request.UserID(r))
20 | if err != nil {
21 | html.ServerError(w, r, err)
22 | return
23 | }
24 |
25 | dbSize, dbErr := h.store.DBSize()
26 |
27 | sess := session.New(h.store, request.SessionID(r))
28 | view := view.New(h.tpl, r, sess)
29 | view.Set("version", version.Version)
30 | view.Set("commit", version.Commit)
31 | view.Set("build_date", version.BuildDate)
32 | view.Set("menu", "settings")
33 | view.Set("user", user)
34 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
35 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
36 | view.Set("globalConfigOptions", config.Opts.SortedOptions(true))
37 | view.Set("postgres_version", h.store.DatabaseVersion())
38 | view.Set("go_version", runtime.Version())
39 |
40 | if dbErr != nil {
41 | view.Set("db_usage", dbErr)
42 | } else {
43 | view.Set("db_usage", dbSize)
44 | }
45 |
46 | html.OK(w, r, view.Render("about"))
47 | }
48 |
--------------------------------------------------------------------------------
/internal/ui/api_key_create.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/ui/form"
12 | "miniflux.app/v2/internal/ui/session"
13 | "miniflux.app/v2/internal/ui/view"
14 | )
15 |
16 | func (h *handler) showCreateAPIKeyPage(w http.ResponseWriter, r *http.Request) {
17 | user, err := h.store.UserByID(request.UserID(r))
18 | if err != nil {
19 | html.ServerError(w, r, err)
20 | return
21 | }
22 |
23 | sess := session.New(h.store, request.SessionID(r))
24 | view := view.New(h.tpl, r, sess)
25 | view.Set("form", &form.APIKeyForm{})
26 | view.Set("menu", "settings")
27 | view.Set("user", user)
28 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
29 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
30 |
31 | html.OK(w, r, view.Render("create_api_key"))
32 | }
33 |
--------------------------------------------------------------------------------
/internal/ui/api_key_list.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/ui/session"
12 | "miniflux.app/v2/internal/ui/view"
13 | )
14 |
15 | func (h *handler) showAPIKeysPage(w http.ResponseWriter, r *http.Request) {
16 | user, err := h.store.UserByID(request.UserID(r))
17 | if err != nil {
18 | html.ServerError(w, r, err)
19 | return
20 | }
21 |
22 | apiKeys, err := h.store.APIKeys(user.ID)
23 | if err != nil {
24 | html.ServerError(w, r, err)
25 | return
26 | }
27 |
28 | sess := session.New(h.store, request.SessionID(r))
29 | view := view.New(h.tpl, r, sess)
30 | view.Set("apiKeys", apiKeys)
31 | view.Set("menu", "settings")
32 | view.Set("user", user)
33 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
34 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
35 |
36 | html.OK(w, r, view.Render("api_keys"))
37 | }
38 |
--------------------------------------------------------------------------------
/internal/ui/api_key_remove.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/http/route"
12 | )
13 |
14 | func (h *handler) deleteAPIKey(w http.ResponseWriter, r *http.Request) {
15 | keyID := request.RouteInt64Param(r, "keyID")
16 | if err := h.store.DeleteAPIKey(request.UserID(r), keyID); err != nil {
17 | html.ServerError(w, r, err)
18 | return
19 | }
20 |
21 | html.Redirect(w, r, route.Path(h.router, "apiKeys"))
22 | }
23 |
--------------------------------------------------------------------------------
/internal/ui/category_create.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/ui/session"
12 | "miniflux.app/v2/internal/ui/view"
13 | )
14 |
15 | func (h *handler) showCreateCategoryPage(w http.ResponseWriter, r *http.Request) {
16 | user, err := h.store.UserByID(request.UserID(r))
17 | if err != nil {
18 | html.ServerError(w, r, err)
19 | return
20 | }
21 |
22 | sess := session.New(h.store, request.SessionID(r))
23 | view := view.New(h.tpl, r, sess)
24 | view.Set("menu", "categories")
25 | view.Set("user", user)
26 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
27 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
28 |
29 | html.OK(w, r, view.Render("create_category"))
30 | }
31 |
--------------------------------------------------------------------------------
/internal/ui/category_edit.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/ui/form"
12 | "miniflux.app/v2/internal/ui/session"
13 | "miniflux.app/v2/internal/ui/view"
14 | )
15 |
16 | func (h *handler) showEditCategoryPage(w http.ResponseWriter, r *http.Request) {
17 | user, err := h.store.UserByID(request.UserID(r))
18 | if err != nil {
19 | html.ServerError(w, r, err)
20 | return
21 | }
22 |
23 | category, err := h.store.Category(request.UserID(r), request.RouteInt64Param(r, "categoryID"))
24 | if err != nil {
25 | html.ServerError(w, r, err)
26 | return
27 | }
28 |
29 | if category == nil {
30 | html.NotFound(w, r)
31 | return
32 | }
33 |
34 | categoryForm := form.CategoryForm{
35 | Title: category.Title,
36 | HideGlobally: category.HideGlobally,
37 | }
38 |
39 | sess := session.New(h.store, request.SessionID(r))
40 | view := view.New(h.tpl, r, sess)
41 | view.Set("form", categoryForm)
42 | view.Set("category", category)
43 | view.Set("menu", "categories")
44 | view.Set("user", user)
45 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
46 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
47 |
48 | html.OK(w, r, view.Render("edit_category"))
49 | }
50 |
--------------------------------------------------------------------------------
/internal/ui/category_feeds.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/ui/session"
12 | "miniflux.app/v2/internal/ui/view"
13 | )
14 |
15 | func (h *handler) showCategoryFeedsPage(w http.ResponseWriter, r *http.Request) {
16 | user, err := h.store.UserByID(request.UserID(r))
17 | if err != nil {
18 | html.ServerError(w, r, err)
19 | return
20 | }
21 |
22 | categoryID := request.RouteInt64Param(r, "categoryID")
23 | category, err := h.store.Category(request.UserID(r), categoryID)
24 | if err != nil {
25 | html.ServerError(w, r, err)
26 | return
27 | }
28 |
29 | if category == nil {
30 | html.NotFound(w, r)
31 | return
32 | }
33 |
34 | feeds, err := h.store.FeedsByCategoryWithCounters(user.ID, categoryID)
35 | if err != nil {
36 | html.ServerError(w, r, err)
37 | return
38 | }
39 |
40 | sess := session.New(h.store, request.SessionID(r))
41 | view := view.New(h.tpl, r, sess)
42 | view.Set("category", category)
43 | view.Set("feeds", feeds)
44 | view.Set("total", len(feeds))
45 | view.Set("menu", "categories")
46 | view.Set("user", user)
47 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
48 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
49 |
50 | html.OK(w, r, view.Render("category_feeds"))
51 | }
52 |
--------------------------------------------------------------------------------
/internal/ui/category_list.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/ui/session"
12 | "miniflux.app/v2/internal/ui/view"
13 | )
14 |
15 | func (h *handler) showCategoryListPage(w http.ResponseWriter, r *http.Request) {
16 | user, err := h.store.UserByID(request.UserID(r))
17 | if err != nil {
18 | html.ServerError(w, r, err)
19 | return
20 | }
21 |
22 | categories, err := h.store.CategoriesWithFeedCount(user.ID)
23 | if err != nil {
24 | html.ServerError(w, r, err)
25 | return
26 | }
27 |
28 | sess := session.New(h.store, request.SessionID(r))
29 | view := view.New(h.tpl, r, sess)
30 | view.Set("categories", categories)
31 | view.Set("total", len(categories))
32 | view.Set("menu", "categories")
33 | view.Set("user", user)
34 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
35 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
36 |
37 | html.OK(w, r, view.Render("categories"))
38 | }
39 |
--------------------------------------------------------------------------------
/internal/ui/category_mark_as_read.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 | "time"
9 |
10 | "miniflux.app/v2/internal/http/request"
11 | "miniflux.app/v2/internal/http/response/html"
12 | "miniflux.app/v2/internal/http/route"
13 | )
14 |
15 | func (h *handler) markCategoryAsRead(w http.ResponseWriter, r *http.Request) {
16 | userID := request.UserID(r)
17 | categoryID := request.RouteInt64Param(r, "categoryID")
18 |
19 | category, err := h.store.Category(userID, categoryID)
20 | if err != nil {
21 | html.ServerError(w, r, err)
22 | return
23 | }
24 |
25 | if category == nil {
26 | html.NotFound(w, r)
27 | return
28 | }
29 |
30 | if err = h.store.MarkCategoryAsRead(userID, categoryID, time.Now()); err != nil {
31 | html.ServerError(w, r, err)
32 | return
33 | }
34 |
35 | html.Redirect(w, r, route.Path(h.router, "categories"))
36 | }
37 |
--------------------------------------------------------------------------------
/internal/ui/category_remove.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/http/route"
12 | )
13 |
14 | func (h *handler) removeCategory(w http.ResponseWriter, r *http.Request) {
15 | user, err := h.store.UserByID(request.UserID(r))
16 | if err != nil {
17 | html.ServerError(w, r, err)
18 | return
19 | }
20 |
21 | categoryID := request.RouteInt64Param(r, "categoryID")
22 | category, err := h.store.Category(request.UserID(r), categoryID)
23 | if err != nil {
24 | html.ServerError(w, r, err)
25 | return
26 | }
27 |
28 | if category == nil {
29 | html.NotFound(w, r)
30 | return
31 | }
32 |
33 | if err := h.store.RemoveCategory(user.ID, category.ID); err != nil {
34 | html.ServerError(w, r, err)
35 | return
36 | }
37 |
38 | html.Redirect(w, r, route.Path(h.router, "categories"))
39 | }
40 |
--------------------------------------------------------------------------------
/internal/ui/category_remove_feed.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/http/route"
12 | )
13 |
14 | func (h *handler) removeCategoryFeed(w http.ResponseWriter, r *http.Request) {
15 | feedID := request.RouteInt64Param(r, "feedID")
16 | categoryID := request.RouteInt64Param(r, "categoryID")
17 |
18 | if !h.store.CategoryFeedExists(request.UserID(r), categoryID, feedID) {
19 | html.NotFound(w, r)
20 | return
21 | }
22 |
23 | if err := h.store.RemoveFeed(request.UserID(r), feedID); err != nil {
24 | html.ServerError(w, r, err)
25 | return
26 | }
27 |
28 | html.Redirect(w, r, route.Path(h.router, "categoryFeeds", "categoryID", categoryID))
29 | }
30 |
--------------------------------------------------------------------------------
/internal/ui/entry_enclosure_save_position.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | json_parser "encoding/json"
8 | "net/http"
9 |
10 | "miniflux.app/v2/internal/http/request"
11 | "miniflux.app/v2/internal/http/response/json"
12 | )
13 |
14 | func (h *handler) saveEnclosureProgression(w http.ResponseWriter, r *http.Request) {
15 | enclosureID := request.RouteInt64Param(r, "enclosureID")
16 | enclosure, err := h.store.GetEnclosure(enclosureID)
17 | if err != nil {
18 | json.ServerError(w, r, err)
19 | return
20 | }
21 |
22 | if enclosure == nil {
23 | json.NotFound(w, r)
24 | return
25 | }
26 |
27 | type enclosurePositionSaveRequest struct {
28 | Progression int64 `json:"progression"`
29 | }
30 |
31 | var postData enclosurePositionSaveRequest
32 | if err := json_parser.NewDecoder(r.Body).Decode(&postData); err != nil {
33 | json.ServerError(w, r, err)
34 | return
35 | }
36 | enclosure.MediaProgression = postData.Progression
37 |
38 | if err := h.store.UpdateEnclosure(enclosure); err != nil {
39 | json.ServerError(w, r, err)
40 | return
41 | }
42 |
43 | json.Created(w, r, map[string]string{"message": "saved"})
44 | }
45 |
--------------------------------------------------------------------------------
/internal/ui/entry_save.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/json"
11 | "miniflux.app/v2/internal/integration"
12 | "miniflux.app/v2/internal/model"
13 | )
14 |
15 | func (h *handler) saveEntry(w http.ResponseWriter, r *http.Request) {
16 | entryID := request.RouteInt64Param(r, "entryID")
17 | builder := h.store.NewEntryQueryBuilder(request.UserID(r))
18 | builder.WithEntryID(entryID)
19 | builder.WithoutStatus(model.EntryStatusRemoved)
20 |
21 | entry, err := builder.GetEntry()
22 | if err != nil {
23 | json.ServerError(w, r, err)
24 | return
25 | }
26 |
27 | if entry == nil {
28 | json.NotFound(w, r)
29 | return
30 | }
31 |
32 | userIntegrations, err := h.store.Integration(request.UserID(r))
33 | if err != nil {
34 | json.ServerError(w, r, err)
35 | return
36 | }
37 |
38 | go integration.SendEntry(entry, userIntegrations)
39 |
40 | json.Created(w, r, map[string]string{"message": "saved"})
41 | }
42 |
--------------------------------------------------------------------------------
/internal/ui/entry_toggle_bookmark.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/json"
11 | )
12 |
13 | func (h *handler) toggleBookmark(w http.ResponseWriter, r *http.Request) {
14 | entryID := request.RouteInt64Param(r, "entryID")
15 | if err := h.store.ToggleBookmark(request.UserID(r), entryID); err != nil {
16 | json.ServerError(w, r, err)
17 | return
18 | }
19 |
20 | json.OK(w, r, "OK")
21 | }
22 |
--------------------------------------------------------------------------------
/internal/ui/entry_update_status.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | json_parser "encoding/json"
8 | "net/http"
9 |
10 | "miniflux.app/v2/internal/http/request"
11 | "miniflux.app/v2/internal/http/response/json"
12 | "miniflux.app/v2/internal/model"
13 | "miniflux.app/v2/internal/validator"
14 | )
15 |
16 | func (h *handler) updateEntriesStatus(w http.ResponseWriter, r *http.Request) {
17 | var entriesStatusUpdateRequest model.EntriesStatusUpdateRequest
18 | if err := json_parser.NewDecoder(r.Body).Decode(&entriesStatusUpdateRequest); err != nil {
19 | json.BadRequest(w, r, err)
20 | return
21 | }
22 |
23 | if err := validator.ValidateEntriesStatusUpdateRequest(&entriesStatusUpdateRequest); err != nil {
24 | json.BadRequest(w, r, err)
25 | return
26 | }
27 |
28 | count, err := h.store.SetEntriesStatusCount(request.UserID(r), entriesStatusUpdateRequest.EntryIDs, entriesStatusUpdateRequest.Status)
29 | if err != nil {
30 | json.ServerError(w, r, err)
31 | return
32 | }
33 |
34 | json.OK(w, r, count)
35 | }
36 |
--------------------------------------------------------------------------------
/internal/ui/feed_icon.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 | "time"
9 |
10 | "miniflux.app/v2/internal/http/request"
11 | "miniflux.app/v2/internal/http/response"
12 | "miniflux.app/v2/internal/http/response/html"
13 | )
14 |
15 | func (h *handler) showFeedIcon(w http.ResponseWriter, r *http.Request) {
16 | externalIconID := request.RouteStringParam(r, "externalIconID")
17 | icon, err := h.store.IconByExternalID(externalIconID)
18 | if err != nil {
19 | html.ServerError(w, r, err)
20 | return
21 | }
22 |
23 | if icon == nil {
24 | html.NotFound(w, r)
25 | return
26 | }
27 |
28 | response.New(w, r).WithCaching(icon.Hash, 72*time.Hour, func(b *response.Builder) {
29 | b.WithHeader("Content-Security-Policy", response.ContentSecurityPolicyForUntrustedContent)
30 | b.WithHeader("Content-Type", icon.MimeType)
31 | b.WithBody(icon.Content)
32 | if icon.MimeType != "image/svg+xml" {
33 | b.WithoutCompression()
34 | }
35 | b.Write()
36 | })
37 | }
38 |
--------------------------------------------------------------------------------
/internal/ui/feed_list.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/ui/session"
12 | "miniflux.app/v2/internal/ui/view"
13 | )
14 |
15 | func (h *handler) showFeedsPage(w http.ResponseWriter, r *http.Request) {
16 | user, err := h.store.UserByID(request.UserID(r))
17 | if err != nil {
18 | html.ServerError(w, r, err)
19 | return
20 | }
21 |
22 | feeds, err := h.store.FeedsWithCounters(user.ID)
23 | if err != nil {
24 | html.ServerError(w, r, err)
25 | return
26 | }
27 |
28 | sess := session.New(h.store, request.SessionID(r))
29 | view := view.New(h.tpl, r, sess)
30 | view.Set("feeds", feeds)
31 | view.Set("total", len(feeds))
32 | view.Set("menu", "feeds")
33 | view.Set("user", user)
34 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
35 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
36 |
37 | html.OK(w, r, view.Render("feeds"))
38 | }
39 |
--------------------------------------------------------------------------------
/internal/ui/feed_mark_as_read.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/http/route"
12 | )
13 |
14 | func (h *handler) markFeedAsRead(w http.ResponseWriter, r *http.Request) {
15 | feedID := request.RouteInt64Param(r, "feedID")
16 | userID := request.UserID(r)
17 |
18 | feed, err := h.store.FeedByID(userID, feedID)
19 |
20 | if err != nil {
21 | html.ServerError(w, r, err)
22 | return
23 | }
24 |
25 | if feed == nil {
26 | html.NotFound(w, r)
27 | return
28 | }
29 |
30 | if err = h.store.MarkFeedAsRead(userID, feedID, feed.CheckedAt); err != nil {
31 | html.ServerError(w, r, err)
32 | return
33 | }
34 |
35 | html.Redirect(w, r, route.Path(h.router, "feeds"))
36 | }
37 |
--------------------------------------------------------------------------------
/internal/ui/feed_remove.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/http/route"
12 | )
13 |
14 | func (h *handler) removeFeed(w http.ResponseWriter, r *http.Request) {
15 | feedID := request.RouteInt64Param(r, "feedID")
16 |
17 | if !h.store.FeedExists(request.UserID(r), feedID) {
18 | html.NotFound(w, r)
19 | return
20 | }
21 |
22 | if err := h.store.RemoveFeed(request.UserID(r), feedID); err != nil {
23 | html.ServerError(w, r, err)
24 | return
25 | }
26 |
27 | html.Redirect(w, r, route.Path(h.router, "feeds"))
28 | }
29 |
--------------------------------------------------------------------------------
/internal/ui/form/api_key.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package form // import "miniflux.app/v2/internal/ui/form"
5 |
6 | import (
7 | "net/http"
8 | "strings"
9 | )
10 |
11 | // APIKeyForm represents the API Key form.
12 | type APIKeyForm struct {
13 | Description string
14 | }
15 |
16 | // NewAPIKeyForm returns a new APIKeyForm.
17 | func NewAPIKeyForm(r *http.Request) *APIKeyForm {
18 | return &APIKeyForm{
19 | Description: strings.TrimSpace(r.FormValue("description")),
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/internal/ui/form/auth.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package form // import "miniflux.app/v2/internal/ui/form"
5 |
6 | import (
7 | "net/http"
8 | "strings"
9 |
10 | "miniflux.app/v2/internal/locale"
11 | )
12 |
13 | // AuthForm represents the authentication form.
14 | type AuthForm struct {
15 | Username string
16 | Password string
17 | }
18 |
19 | // Validate makes sure the form values are valid.
20 | func (a AuthForm) Validate() *locale.LocalizedError {
21 | if a.Username == "" || a.Password == "" {
22 | return locale.NewLocalizedError("error.fields_mandatory")
23 | }
24 |
25 | return nil
26 | }
27 |
28 | // NewAuthForm returns a new AuthForm.
29 | func NewAuthForm(r *http.Request) *AuthForm {
30 | return &AuthForm{
31 | Username: strings.TrimSpace(r.FormValue("username")),
32 | Password: strings.TrimSpace(r.FormValue("password")),
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/internal/ui/form/category.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package form // import "miniflux.app/v2/internal/ui/form"
5 |
6 | import (
7 | "net/http"
8 | )
9 |
10 | // CategoryForm represents a feed form in the UI
11 | type CategoryForm struct {
12 | Title string
13 | HideGlobally bool
14 | }
15 |
16 | // NewCategoryForm returns a new CategoryForm.
17 | func NewCategoryForm(r *http.Request) *CategoryForm {
18 | return &CategoryForm{
19 | Title: r.FormValue("title"),
20 | HideGlobally: r.FormValue("hide_globally") == "1",
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/internal/ui/form/webauthn.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package form // import "miniflux.app/v2/internal/ui/form"
5 |
6 | import (
7 | "net/http"
8 | )
9 |
10 | // WebauthnForm represents a credential rename form in the UI
11 | type WebauthnForm struct {
12 | Name string
13 | }
14 |
15 | // NewWebauthnForm returns a new WebnauthnForm.
16 | func NewWebauthnForm(r *http.Request) *WebauthnForm {
17 | return &WebauthnForm{
18 | Name: r.FormValue("name"),
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/internal/ui/handler.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "miniflux.app/v2/internal/storage"
8 | "miniflux.app/v2/internal/template"
9 | "miniflux.app/v2/internal/worker"
10 |
11 | "github.com/gorilla/mux"
12 | )
13 |
14 | type handler struct {
15 | router *mux.Router
16 | store *storage.Storage
17 | tpl *template.Engine
18 | pool *worker.Pool
19 | }
20 |
--------------------------------------------------------------------------------
/internal/ui/history_flush.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/json"
11 | )
12 |
13 | func (h *handler) flushHistory(w http.ResponseWriter, r *http.Request) {
14 | err := h.store.FlushHistory(request.UserID(r))
15 | if err != nil {
16 | json.ServerError(w, r, err)
17 | return
18 | }
19 |
20 | json.OK(w, r, "OK")
21 | }
22 |
--------------------------------------------------------------------------------
/internal/ui/login_show.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/http/route"
12 | "miniflux.app/v2/internal/ui/session"
13 | "miniflux.app/v2/internal/ui/view"
14 | )
15 |
16 | func (h *handler) showLoginPage(w http.ResponseWriter, r *http.Request) {
17 | if request.IsAuthenticated(r) {
18 | user, err := h.store.UserByID(request.UserID(r))
19 | if err != nil {
20 | html.ServerError(w, r, err)
21 | return
22 | }
23 |
24 | html.Redirect(w, r, route.Path(h.router, user.DefaultHomePage))
25 | return
26 | }
27 |
28 | sess := session.New(h.store, request.SessionID(r))
29 | view := view.New(h.tpl, r, sess)
30 | html.OK(w, r, view.Render("login"))
31 | }
32 |
--------------------------------------------------------------------------------
/internal/ui/logout.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/config"
10 | "miniflux.app/v2/internal/http/cookie"
11 | "miniflux.app/v2/internal/http/request"
12 | "miniflux.app/v2/internal/http/response/html"
13 | "miniflux.app/v2/internal/http/route"
14 | "miniflux.app/v2/internal/ui/session"
15 | )
16 |
17 | func (h *handler) logout(w http.ResponseWriter, r *http.Request) {
18 | sess := session.New(h.store, request.SessionID(r))
19 | user, err := h.store.UserByID(request.UserID(r))
20 | if err != nil {
21 | html.ServerError(w, r, err)
22 | return
23 | }
24 |
25 | sess.SetLanguage(user.Language)
26 | sess.SetTheme(user.Theme)
27 |
28 | if err := h.store.RemoveUserSessionByToken(user.ID, request.UserSessionToken(r)); err != nil {
29 | html.ServerError(w, r, err)
30 | return
31 | }
32 |
33 | http.SetCookie(w, cookie.Expired(
34 | cookie.CookieUserSessionID,
35 | config.Opts.HTTPS,
36 | config.Opts.BasePath(),
37 | ))
38 |
39 | html.Redirect(w, r, route.Path(h.router, "login"))
40 | }
41 |
--------------------------------------------------------------------------------
/internal/ui/oauth2.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "context"
8 |
9 | "miniflux.app/v2/internal/config"
10 | "miniflux.app/v2/internal/oauth2"
11 | )
12 |
13 | func getOAuth2Manager(ctx context.Context) *oauth2.Manager {
14 | return oauth2.NewManager(
15 | ctx,
16 | config.Opts.OAuth2ClientID(),
17 | config.Opts.OAuth2ClientSecret(),
18 | config.Opts.OAuth2RedirectURL(),
19 | config.Opts.OIDCDiscoveryEndpoint(),
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/internal/ui/oauth2_redirect.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "log/slog"
8 | "net/http"
9 |
10 | "miniflux.app/v2/internal/http/request"
11 | "miniflux.app/v2/internal/http/response/html"
12 | "miniflux.app/v2/internal/http/route"
13 | "miniflux.app/v2/internal/oauth2"
14 | "miniflux.app/v2/internal/ui/session"
15 | )
16 |
17 | func (h *handler) oauth2Redirect(w http.ResponseWriter, r *http.Request) {
18 | sess := session.New(h.store, request.SessionID(r))
19 |
20 | provider := request.RouteStringParam(r, "provider")
21 | if provider == "" {
22 | slog.Warn("Invalid or missing OAuth2 provider")
23 | html.Redirect(w, r, route.Path(h.router, "login"))
24 | return
25 | }
26 |
27 | authProvider, err := getOAuth2Manager(r.Context()).FindProvider(provider)
28 | if err != nil {
29 | slog.Error("Unable to initialize OAuth2 provider",
30 | slog.String("provider", provider),
31 | slog.Any("error", err),
32 | )
33 | html.Redirect(w, r, route.Path(h.router, "login"))
34 | return
35 | }
36 |
37 | auth := oauth2.GenerateAuthorization(authProvider.GetConfig())
38 |
39 | sess.SetOAuth2State(auth.State())
40 | sess.SetOAuth2CodeVerifier(auth.CodeVerifier())
41 |
42 | html.Redirect(w, r, auth.RedirectURL())
43 | }
44 |
--------------------------------------------------------------------------------
/internal/ui/offline.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/ui/session"
12 | "miniflux.app/v2/internal/ui/view"
13 | )
14 |
15 | func (h *handler) showOfflinePage(w http.ResponseWriter, r *http.Request) {
16 | sess := session.New(h.store, request.SessionID(r))
17 | view := view.New(h.tpl, r, sess)
18 | html.OK(w, r, view.Render("offline"))
19 | }
20 |
--------------------------------------------------------------------------------
/internal/ui/opml_export.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/http/response/xml"
12 | "miniflux.app/v2/internal/reader/opml"
13 | )
14 |
15 | func (h *handler) exportFeeds(w http.ResponseWriter, r *http.Request) {
16 | opmlExport, err := opml.NewHandler(h.store).Export(request.UserID(r))
17 | if err != nil {
18 | html.ServerError(w, r, err)
19 | return
20 | }
21 |
22 | xml.Attachment(w, r, "feeds.opml", opmlExport)
23 | }
24 |
--------------------------------------------------------------------------------
/internal/ui/opml_import.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/ui/session"
12 | "miniflux.app/v2/internal/ui/view"
13 | )
14 |
15 | func (h *handler) showImportPage(w http.ResponseWriter, r *http.Request) {
16 | user, err := h.store.UserByID(request.UserID(r))
17 | if err != nil {
18 | html.ServerError(w, r, err)
19 | return
20 | }
21 |
22 | sess := session.New(h.store, request.SessionID(r))
23 | view := view.New(h.tpl, r, sess)
24 | view.Set("menu", "feeds")
25 | view.Set("user", user)
26 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
27 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
28 |
29 | html.OK(w, r, view.Render("import"))
30 | }
31 |
--------------------------------------------------------------------------------
/internal/ui/pagination.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | type pagination struct {
7 | Route string
8 | Total int
9 | Offset int
10 | ItemsPerPage int
11 | ShowNext bool
12 | ShowLast bool
13 | ShowFirst bool
14 | ShowPrev bool
15 | NextOffset int
16 | LastOffset int
17 | PrevOffset int
18 | FirstOffset int
19 | SearchQuery string
20 | }
21 |
22 | func getPagination(route string, total, offset, nbItemsPerPage int) pagination {
23 | nextOffset := 0
24 | prevOffset := 0
25 |
26 | firstOffset := 0
27 | lastOffset := (total / nbItemsPerPage) * nbItemsPerPage
28 | if lastOffset == total {
29 | lastOffset -= nbItemsPerPage
30 | }
31 |
32 | showNext := (total - offset) > nbItemsPerPage
33 | showPrev := offset > 0
34 | showLast := showNext
35 | showFirst := showPrev
36 |
37 | if showNext {
38 | nextOffset = offset + nbItemsPerPage
39 | }
40 |
41 | if showPrev {
42 | prevOffset = offset - nbItemsPerPage
43 | }
44 |
45 | return pagination{
46 | Route: route,
47 | Total: total,
48 | Offset: offset,
49 | ItemsPerPage: nbItemsPerPage,
50 | ShowNext: showNext,
51 | ShowLast: showLast,
52 | NextOffset: nextOffset,
53 | LastOffset: lastOffset,
54 | ShowPrev: showPrev,
55 | ShowFirst: showFirst,
56 | PrevOffset: prevOffset,
57 | FirstOffset: firstOffset,
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/internal/ui/session_list.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/ui/session"
12 | "miniflux.app/v2/internal/ui/view"
13 | )
14 |
15 | func (h *handler) showSessionsPage(w http.ResponseWriter, r *http.Request) {
16 | user, err := h.store.UserByID(request.UserID(r))
17 | if err != nil {
18 | html.ServerError(w, r, err)
19 | return
20 | }
21 |
22 | sessions, err := h.store.UserSessions(user.ID)
23 | if err != nil {
24 | html.ServerError(w, r, err)
25 | return
26 | }
27 |
28 | sessions.UseTimezone(user.Timezone)
29 |
30 | sess := session.New(h.store, request.SessionID(r))
31 | view := view.New(h.tpl, r, sess)
32 | view.Set("currentSessionToken", request.UserSessionToken(r))
33 | view.Set("sessions", sessions)
34 | view.Set("menu", "settings")
35 | view.Set("user", user)
36 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
37 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
38 |
39 | html.OK(w, r, view.Render("sessions"))
40 | }
41 |
--------------------------------------------------------------------------------
/internal/ui/session_remove.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/http/route"
12 | )
13 |
14 | func (h *handler) removeSession(w http.ResponseWriter, r *http.Request) {
15 | sessionID := request.RouteInt64Param(r, "sessionID")
16 | err := h.store.RemoveUserSessionByID(request.UserID(r), sessionID)
17 | if err != nil {
18 | html.ServerError(w, r, err)
19 | return
20 | }
21 |
22 | html.Redirect(w, r, route.Path(h.router, "sessions"))
23 | }
24 |
--------------------------------------------------------------------------------
/internal/ui/static/bin/favicon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/ui/static/bin/favicon-16.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/favicon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/ui/static/bin/favicon-32.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/ui/static/bin/favicon.ico
--------------------------------------------------------------------------------
/internal/ui/static/bin/icon-120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/ui/static/bin/icon-120.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/ui/static/bin/icon-128.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/icon-152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/ui/static/bin/icon-152.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/icon-167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/ui/static/bin/icon-167.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/icon-180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/ui/static/bin/icon-180.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/ui/static/bin/icon-192.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/ui/static/bin/icon-512.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/maskable-icon-120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/ui/static/bin/maskable-icon-120.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/maskable-icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/ui/static/bin/maskable-icon-192.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/maskable-icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miniflux/v2/0369f03940273646bb910673fc5f441297f28696/internal/ui/static/bin/maskable-icon-512.png
--------------------------------------------------------------------------------
/internal/ui/static/css/sans_serif.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --entry-content-font-weight: 400;
3 | --entry-content-font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
4 | --entry-content-quote-font-family: var(--entry-content-font-family);
5 | }
6 |
--------------------------------------------------------------------------------
/internal/ui/static/css/serif.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --entry-content-font-weight: 300;
3 | --entry-content-font-family: Georgia, 'Times New Roman', Times, serif;
4 | --entry-content-quote-font-family: var(--entry-content-font-family);
5 | }
--------------------------------------------------------------------------------
/internal/ui/static/js/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2020": true
5 | },
6 | "rules": {
7 | "indent": ["error", 4]
8 | }
9 | }
--------------------------------------------------------------------------------
/internal/ui/static/js/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "bitwise": true,
3 | "browser": true,
4 | "eqeqeq": true,
5 | "esversion": 11,
6 | "freeze": true,
7 | "latedef": "nofunc",
8 | "noarg": true,
9 | "nocomma": true,
10 | "nonbsp": true,
11 | "nonew": true,
12 | "noreturnawait": true,
13 | "shadow": true,
14 | "varstmt": true
15 | }
--------------------------------------------------------------------------------
/internal/ui/static/js/request_builder.js:
--------------------------------------------------------------------------------
1 | class RequestBuilder {
2 | constructor(url) {
3 | this.callback = null;
4 | this.url = url;
5 | this.options = {
6 | method: "POST",
7 | cache: "no-cache",
8 | credentials: "include",
9 | body: null,
10 | headers: new Headers({
11 | "Content-Type": "application/json",
12 | "X-Csrf-Token": getCsrfToken()
13 | })
14 | };
15 | }
16 |
17 | withHttpMethod(method) {
18 | this.options.method = method;
19 | return this;
20 | }
21 |
22 | withBody(body) {
23 | this.options.body = JSON.stringify(body);
24 | return this;
25 | }
26 |
27 | withCallback(callback) {
28 | this.callback = callback;
29 | return this;
30 | }
31 |
32 | execute() {
33 | fetch(new Request(this.url, this.options)).then((response) => {
34 | if (this.callback) {
35 | this.callback(response);
36 | }
37 | });
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/internal/ui/static/js/tt.js:
--------------------------------------------------------------------------------
1 | let ttpolicy;
2 | if (window.trustedTypes && trustedTypes.createPolicy) {
3 | //TODO: use an allow-list for `createScriptURL`
4 | if (!ttpolicy) {
5 | ttpolicy = trustedTypes.createPolicy('ttpolicy', {
6 | createScriptURL: src => src,
7 | createHTML: html => html,
8 | });
9 | }
10 | } else {
11 | ttpolicy = {
12 | createScriptURL: src => src,
13 | createHTML: html => html,
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/internal/ui/static_app_icon.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 | "path/filepath"
9 | "time"
10 |
11 | "miniflux.app/v2/internal/http/request"
12 | "miniflux.app/v2/internal/http/response"
13 | "miniflux.app/v2/internal/http/response/html"
14 | "miniflux.app/v2/internal/ui/static"
15 | )
16 |
17 | func (h *handler) showAppIcon(w http.ResponseWriter, r *http.Request) {
18 | filename := request.RouteStringParam(r, "filename")
19 | etag, err := static.GetBinaryFileChecksum(filename)
20 | if err != nil {
21 | html.NotFound(w, r)
22 | return
23 | }
24 |
25 | response.New(w, r).WithCaching(etag, 72*time.Hour, func(b *response.Builder) {
26 | blob, err := static.LoadBinaryFile(filename)
27 | if err != nil {
28 | html.ServerError(w, r, err)
29 | return
30 | }
31 |
32 | switch filepath.Ext(filename) {
33 | case ".png":
34 | b.WithoutCompression()
35 | b.WithHeader("Content-Type", "image/png")
36 | case ".svg":
37 | b.WithHeader("Content-Type", "image/svg+xml")
38 | }
39 |
40 | b.WithBody(blob)
41 | b.Write()
42 | })
43 | }
44 |
--------------------------------------------------------------------------------
/internal/ui/static_favicon.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 | "time"
9 |
10 | "miniflux.app/v2/internal/http/response"
11 | "miniflux.app/v2/internal/http/response/html"
12 | "miniflux.app/v2/internal/ui/static"
13 | )
14 |
15 | func (h *handler) showFavicon(w http.ResponseWriter, r *http.Request) {
16 | etag, err := static.GetBinaryFileChecksum("favicon.ico")
17 | if err != nil {
18 | html.NotFound(w, r)
19 | return
20 | }
21 |
22 | response.New(w, r).WithCaching(etag, 48*time.Hour, func(b *response.Builder) {
23 | blob, err := static.LoadBinaryFile("favicon.ico")
24 | if err != nil {
25 | html.ServerError(w, r, err)
26 | return
27 | }
28 |
29 | b.WithHeader("Content-Type", "image/x-icon")
30 | b.WithoutCompression()
31 | b.WithBody(blob)
32 | b.Write()
33 | })
34 | }
35 |
--------------------------------------------------------------------------------
/internal/ui/static_javascript.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "fmt"
8 | "net/http"
9 | "strings"
10 | "time"
11 |
12 | "miniflux.app/v2/internal/http/request"
13 | "miniflux.app/v2/internal/http/response"
14 | "miniflux.app/v2/internal/http/response/html"
15 | "miniflux.app/v2/internal/http/route"
16 | "miniflux.app/v2/internal/ui/static"
17 | )
18 |
19 | const licensePrefix = "//@license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0\n"
20 | const licenseSuffix = "\n//@license-end"
21 |
22 | func (h *handler) showJavascript(w http.ResponseWriter, r *http.Request) {
23 | filename := request.RouteStringParam(r, "name")
24 | etag, found := static.JavascriptBundleChecksums[filename]
25 | if !found {
26 | html.NotFound(w, r)
27 | return
28 | }
29 |
30 | response.New(w, r).WithCaching(etag, 48*time.Hour, func(b *response.Builder) {
31 | contents := static.JavascriptBundles[filename]
32 |
33 | if filename == "service-worker" {
34 | variables := fmt.Sprintf(`const OFFLINE_URL=%q;`, route.Path(h.router, "offline"))
35 | contents = append([]byte(variables), contents...)
36 | }
37 |
38 | // cloning the prefix since `append` mutates its first argument
39 | contents = append([]byte(strings.Clone(licensePrefix)), contents...)
40 | contents = append(contents, []byte(licenseSuffix)...)
41 |
42 | b.WithHeader("Content-Type", "text/javascript; charset=utf-8")
43 | b.WithBody(contents)
44 | b.Write()
45 | })
46 | }
47 |
--------------------------------------------------------------------------------
/internal/ui/static_stylesheet.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 | "time"
9 |
10 | "miniflux.app/v2/internal/http/request"
11 | "miniflux.app/v2/internal/http/response"
12 | "miniflux.app/v2/internal/http/response/html"
13 | "miniflux.app/v2/internal/ui/static"
14 | )
15 |
16 | func (h *handler) showStylesheet(w http.ResponseWriter, r *http.Request) {
17 | filename := request.RouteStringParam(r, "name")
18 | etag, found := static.StylesheetBundleChecksums[filename]
19 | if !found {
20 | html.NotFound(w, r)
21 | return
22 | }
23 |
24 | response.New(w, r).WithCaching(etag, 48*time.Hour, func(b *response.Builder) {
25 | b.WithHeader("Content-Type", "text/css; charset=utf-8")
26 | b.WithBody(static.StylesheetBundles[filename])
27 | b.Write()
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/internal/ui/subscription_add.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/config"
10 | "miniflux.app/v2/internal/http/request"
11 | "miniflux.app/v2/internal/http/response/html"
12 | "miniflux.app/v2/internal/ui/form"
13 | "miniflux.app/v2/internal/ui/session"
14 | "miniflux.app/v2/internal/ui/view"
15 | )
16 |
17 | func (h *handler) showAddSubscriptionPage(w http.ResponseWriter, r *http.Request) {
18 | user, err := h.store.UserByID(request.UserID(r))
19 | if err != nil {
20 | html.ServerError(w, r, err)
21 | return
22 | }
23 |
24 | categories, err := h.store.Categories(user.ID)
25 | if err != nil {
26 | html.ServerError(w, r, err)
27 | return
28 | }
29 |
30 | sess := session.New(h.store, request.SessionID(r))
31 | view := view.New(h.tpl, r, sess)
32 | view.Set("categories", categories)
33 | view.Set("menu", "feeds")
34 | view.Set("user", user)
35 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
36 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
37 | view.Set("defaultUserAgent", config.Opts.HTTPClientUserAgent())
38 | view.Set("form", &form.SubscriptionForm{CategoryID: 0})
39 | view.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyURLConfigured())
40 |
41 | html.OK(w, r, view.Render("add_subscription"))
42 | }
43 |
--------------------------------------------------------------------------------
/internal/ui/unread_mark_all_read.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/json"
11 | )
12 |
13 | func (h *handler) markAllAsRead(w http.ResponseWriter, r *http.Request) {
14 | if err := h.store.MarkGloballyVisibleFeedsAsRead(request.UserID(r)); err != nil {
15 | json.ServerError(w, r, err)
16 | return
17 | }
18 |
19 | json.OK(w, r, "OK")
20 | }
21 |
--------------------------------------------------------------------------------
/internal/ui/user_create.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/ui/form"
12 | "miniflux.app/v2/internal/ui/session"
13 | "miniflux.app/v2/internal/ui/view"
14 | )
15 |
16 | func (h *handler) showCreateUserPage(w http.ResponseWriter, r *http.Request) {
17 | user, err := h.store.UserByID(request.UserID(r))
18 | if err != nil {
19 | html.ServerError(w, r, err)
20 | return
21 | }
22 |
23 | if !user.IsAdmin {
24 | html.Forbidden(w, r)
25 | return
26 | }
27 |
28 | sess := session.New(h.store, request.SessionID(r))
29 | view := view.New(h.tpl, r, sess)
30 | view.Set("form", &form.UserForm{})
31 | view.Set("menu", "settings")
32 | view.Set("user", user)
33 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
34 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
35 |
36 | html.OK(w, r, view.Render("create_user"))
37 | }
38 |
--------------------------------------------------------------------------------
/internal/ui/user_edit.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/ui/form"
12 | "miniflux.app/v2/internal/ui/session"
13 | "miniflux.app/v2/internal/ui/view"
14 | )
15 |
16 | // EditUser shows the form to edit a user.
17 | func (h *handler) showEditUserPage(w http.ResponseWriter, r *http.Request) {
18 | user, err := h.store.UserByID(request.UserID(r))
19 | if err != nil {
20 | html.ServerError(w, r, err)
21 | return
22 | }
23 |
24 | if !user.IsAdmin {
25 | html.Forbidden(w, r)
26 | return
27 | }
28 |
29 | userID := request.RouteInt64Param(r, "userID")
30 | selectedUser, err := h.store.UserByID(userID)
31 | if err != nil {
32 | html.ServerError(w, r, err)
33 | return
34 | }
35 |
36 | if selectedUser == nil {
37 | html.NotFound(w, r)
38 | return
39 | }
40 |
41 | userForm := &form.UserForm{
42 | Username: selectedUser.Username,
43 | IsAdmin: selectedUser.IsAdmin,
44 | }
45 |
46 | sess := session.New(h.store, request.SessionID(r))
47 | view := view.New(h.tpl, r, sess)
48 | view.Set("form", userForm)
49 | view.Set("selected_user", selectedUser)
50 | view.Set("menu", "settings")
51 | view.Set("user", user)
52 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
53 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
54 |
55 | html.OK(w, r, view.Render("edit_user"))
56 | }
57 |
--------------------------------------------------------------------------------
/internal/ui/user_list.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "net/http"
8 |
9 | "miniflux.app/v2/internal/http/request"
10 | "miniflux.app/v2/internal/http/response/html"
11 | "miniflux.app/v2/internal/ui/session"
12 | "miniflux.app/v2/internal/ui/view"
13 | )
14 |
15 | func (h *handler) showUsersPage(w http.ResponseWriter, r *http.Request) {
16 | user, err := h.store.UserByID(request.UserID(r))
17 | if err != nil {
18 | html.ServerError(w, r, err)
19 | return
20 | }
21 |
22 | if !user.IsAdmin {
23 | html.Forbidden(w, r)
24 | return
25 | }
26 |
27 | users, err := h.store.Users()
28 | if err != nil {
29 | html.ServerError(w, r, err)
30 | return
31 | }
32 |
33 | users.UseTimezone(user.Timezone)
34 |
35 | sess := session.New(h.store, request.SessionID(r))
36 | view := view.New(h.tpl, r, sess)
37 | view.Set("users", users)
38 | view.Set("menu", "settings")
39 | view.Set("user", user)
40 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
41 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
42 |
43 | html.OK(w, r, view.Render("users"))
44 | }
45 |
--------------------------------------------------------------------------------
/internal/ui/user_remove.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package ui // import "miniflux.app/v2/internal/ui"
5 |
6 | import (
7 | "errors"
8 | "net/http"
9 |
10 | "miniflux.app/v2/internal/http/request"
11 | "miniflux.app/v2/internal/http/response/html"
12 | "miniflux.app/v2/internal/http/route"
13 | )
14 |
15 | func (h *handler) removeUser(w http.ResponseWriter, r *http.Request) {
16 | loggedUser, err := h.store.UserByID(request.UserID(r))
17 | if err != nil {
18 | html.ServerError(w, r, err)
19 | return
20 | }
21 |
22 | if !loggedUser.IsAdmin {
23 | html.Forbidden(w, r)
24 | return
25 | }
26 |
27 | selectedUserID := request.RouteInt64Param(r, "userID")
28 | selectedUser, err := h.store.UserByID(selectedUserID)
29 | if err != nil {
30 | html.ServerError(w, r, err)
31 | return
32 | }
33 |
34 | if selectedUser == nil {
35 | html.NotFound(w, r)
36 | return
37 | }
38 |
39 | if selectedUser.ID == loggedUser.ID {
40 | html.BadRequest(w, r, errors.New("you cannot remove yourself"))
41 | return
42 | }
43 |
44 | if err := h.store.RemoveUser(selectedUser.ID); err != nil {
45 | html.ServerError(w, r, err)
46 | return
47 | }
48 |
49 | html.Redirect(w, r, route.Path(h.router, "users"))
50 | }
51 |
--------------------------------------------------------------------------------
/internal/validator/api_key.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package validator // import "miniflux.app/v2/internal/validator"
5 |
6 | import (
7 | "miniflux.app/v2/internal/locale"
8 | "miniflux.app/v2/internal/model"
9 | "miniflux.app/v2/internal/storage"
10 | )
11 |
12 | func ValidateAPIKeyCreation(store *storage.Storage, userID int64, request *model.APIKeyCreationRequest) *locale.LocalizedError {
13 | if request.Description == "" {
14 | return locale.NewLocalizedError("error.fields_mandatory")
15 | }
16 |
17 | if store.APIKeyExists(userID, request.Description) {
18 | return locale.NewLocalizedError("error.api_key_already_exists")
19 | }
20 |
21 | return nil
22 | }
23 |
--------------------------------------------------------------------------------
/internal/validator/category.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package validator // import "miniflux.app/v2/internal/validator"
5 |
6 | import (
7 | "miniflux.app/v2/internal/locale"
8 | "miniflux.app/v2/internal/model"
9 | "miniflux.app/v2/internal/storage"
10 | )
11 |
12 | // ValidateCategoryCreation validates category creation.
13 | func ValidateCategoryCreation(store *storage.Storage, userID int64, request *model.CategoryCreationRequest) *locale.LocalizedError {
14 | if request.Title == "" {
15 | return locale.NewLocalizedError("error.title_required")
16 | }
17 |
18 | if store.CategoryTitleExists(userID, request.Title) {
19 | return locale.NewLocalizedError("error.category_already_exists")
20 | }
21 |
22 | return nil
23 | }
24 |
25 | // ValidateCategoryModification validates category modification.
26 | func ValidateCategoryModification(store *storage.Storage, userID, categoryID int64, request *model.CategoryModificationRequest) *locale.LocalizedError {
27 | if request.Title != nil {
28 | if *request.Title == "" {
29 | return locale.NewLocalizedError("error.title_required")
30 | }
31 |
32 | if store.AnotherCategoryExists(userID, categoryID, *request.Title) {
33 | return locale.NewLocalizedError("error.category_already_exists")
34 | }
35 | }
36 |
37 | return nil
38 | }
39 |
--------------------------------------------------------------------------------
/internal/validator/enclosure.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package validator
5 |
6 | import (
7 | "fmt"
8 |
9 | "miniflux.app/v2/internal/model"
10 | )
11 |
12 | func ValidateEnclosureUpdateRequest(request *model.EnclosureUpdateRequest) error {
13 | if request.MediaProgression < 0 {
14 | return fmt.Errorf(`media progression must an positive integer`)
15 | }
16 |
17 | return nil
18 | }
19 |
--------------------------------------------------------------------------------
/internal/validator/subscription.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package validator // import "miniflux.app/v2/internal/validator"
5 |
6 | import (
7 | "miniflux.app/v2/internal/locale"
8 | "miniflux.app/v2/internal/model"
9 | )
10 |
11 | // ValidateSubscriptionDiscovery validates subscription discovery requests.
12 | func ValidateSubscriptionDiscovery(request *model.SubscriptionDiscoveryRequest) *locale.LocalizedError {
13 | if !IsValidURL(request.URL) {
14 | return locale.NewLocalizedError("error.invalid_site_url")
15 | }
16 |
17 | if request.ProxyURL != "" && !IsValidURL(request.ProxyURL) {
18 | return locale.NewLocalizedError("error.invalid_proxy_url")
19 | }
20 |
21 | return nil
22 | }
23 |
--------------------------------------------------------------------------------
/internal/version/version.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package version // import "miniflux.app/v2/internal/version"
5 |
6 | // Variables populated at build time.
7 | var (
8 | Version = "dev"
9 | Commit = "HEAD"
10 | BuildDate = "undefined"
11 | )
12 |
--------------------------------------------------------------------------------
/internal/worker/pool.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package worker // import "miniflux.app/v2/internal/worker"
5 |
6 | import (
7 | "miniflux.app/v2/internal/model"
8 | "miniflux.app/v2/internal/storage"
9 | )
10 |
11 | // Pool handles a pool of workers.
12 | type Pool struct {
13 | queue chan model.Job
14 | }
15 |
16 | // Push send a list of jobs to the queue.
17 | func (p *Pool) Push(jobs model.JobList) {
18 | for _, job := range jobs {
19 | p.queue <- job
20 | }
21 | }
22 |
23 | // NewPool creates a pool of background workers.
24 | func NewPool(store *storage.Storage, nbWorkers int) *Pool {
25 | workerPool := &Pool{
26 | queue: make(chan model.Job),
27 | }
28 |
29 | for i := range nbWorkers {
30 | worker := &Worker{id: i, store: store}
31 | go worker.Run(workerPool.queue)
32 | }
33 |
34 | return workerPool
35 | }
36 |
--------------------------------------------------------------------------------
/internal/worker/worker.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package worker // import "miniflux.app/v2/internal/worker"
5 |
6 | import (
7 | "log/slog"
8 | "time"
9 |
10 | "miniflux.app/v2/internal/config"
11 | "miniflux.app/v2/internal/metric"
12 | "miniflux.app/v2/internal/model"
13 | feedHandler "miniflux.app/v2/internal/reader/handler"
14 | "miniflux.app/v2/internal/storage"
15 | )
16 |
17 | // Worker refreshes a feed in the background.
18 | type Worker struct {
19 | id int
20 | store *storage.Storage
21 | }
22 |
23 | // Run wait for a job and refresh the given feed.
24 | func (w *Worker) Run(c <-chan model.Job) {
25 | slog.Debug("Worker started",
26 | slog.Int("worker_id", w.id),
27 | )
28 |
29 | for {
30 | job := <-c
31 | slog.Debug("Job received by worker",
32 | slog.Int("worker_id", w.id),
33 | slog.Int64("user_id", job.UserID),
34 | slog.Int64("feed_id", job.FeedID),
35 | )
36 |
37 | startTime := time.Now()
38 | localizedError := feedHandler.RefreshFeed(w.store, job.UserID, job.FeedID, false)
39 |
40 | if config.Opts.HasMetricsCollector() {
41 | status := "success"
42 | if localizedError != nil {
43 | status = "error"
44 | }
45 | metric.BackgroundFeedRefreshDuration.WithLabelValues(status).Observe(time.Since(startTime).Seconds())
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package main // import "miniflux.app/v2"
5 |
6 | import (
7 | "miniflux.app/v2/internal/cli"
8 | )
9 |
10 | func main() {
11 | cli.Parse()
12 | }
13 |
--------------------------------------------------------------------------------
/packaging/debian/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM docker.io/golang:1.24-bookworm AS build
2 |
3 | ENV DEBIAN_FRONTEND=noninteractive
4 |
5 | RUN apt-get update -q && \
6 | apt-get install -y -qq build-essential devscripts dh-make debhelper && \
7 | mkdir -p /build/debian
8 |
9 | ADD . /src
10 |
11 | CMD ["/src/packaging/debian/build.sh"]
12 |
--------------------------------------------------------------------------------
/packaging/debian/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | PKG_ARCH=$(dpkg --print-architecture)
4 | PKG_DATE=$(date -R)
5 | PKG_VERSION=$(cd /src && git describe --tags --abbrev=0 | sed 's/^v//')
6 |
7 | echo "PKG_VERSION=$PKG_VERSION"
8 | echo "PKG_ARCH=$PKG_ARCH"
9 | echo "PKG_DATE=$PKG_DATE"
10 |
11 | cd /src
12 |
13 | if [ "$PKG_ARCH" = "armhf" ]; then
14 | make miniflux-no-pie
15 | else
16 | CGO_ENABLED=0 make miniflux
17 | fi
18 |
19 | mkdir -p /build/debian && \
20 | cd /build && \
21 | cp /src/miniflux /build/ && \
22 | cp /src/miniflux.1 /build/ && \
23 | cp /src/LICENSE /build/ && \
24 | cp /src/packaging/miniflux.conf /build/ && \
25 | cp /src/packaging/systemd/miniflux.service /build/debian/ && \
26 | cp /src/packaging/debian/compat /build/debian/compat && \
27 | cp /src/packaging/debian/copyright /build/debian/copyright && \
28 | cp /src/packaging/debian/miniflux.manpages /build/debian/miniflux.manpages && \
29 | cp /src/packaging/debian/miniflux.postinst /build/debian/miniflux.postinst && \
30 | cp /src/packaging/debian/rules /build/debian/rules && \
31 | cp /src/packaging/debian/miniflux.dirs /build/debian/miniflux.dirs && \
32 | echo "miniflux ($PKG_VERSION) experimental; urgency=low" > /build/debian/changelog && \
33 | echo " * Miniflux version $PKG_VERSION" >> /build/debian/changelog && \
34 | echo " -- Frédéric Guillot $PKG_DATE" >> /build/debian/changelog && \
35 | sed "s/__PKG_ARCH__/${PKG_ARCH}/g" /src/packaging/debian/control > /build/debian/control && \
36 | dpkg-buildpackage -us -uc -b && \
37 | lintian --check --color always ../*.deb && \
38 | cp ../*.deb /pkg/
39 |
--------------------------------------------------------------------------------
/packaging/debian/compat:
--------------------------------------------------------------------------------
1 | 10
2 |
--------------------------------------------------------------------------------
/packaging/debian/control:
--------------------------------------------------------------------------------
1 | Source: miniflux
2 | Maintainer: Frederic Guillot
3 | Build-Depends: debhelper (>= 9.20160709) | dh-systemd
4 |
5 | Package: miniflux
6 | Architecture: __PKG_ARCH__
7 | Section: web
8 | Priority: optional
9 | Description: Minimalist Feed Reader
10 | Miniflux is a minimalist and opinionated feed reader
11 | Homepage: https://miniflux.app
12 | Depends: ${misc:Depends}, ${shlibs:Depends}, adduser
13 |
--------------------------------------------------------------------------------
/packaging/debian/copyright:
--------------------------------------------------------------------------------
1 | Files: *
2 | Copyright: 2017-2023 Frederic Guillot
3 | License: Apache
--------------------------------------------------------------------------------
/packaging/debian/miniflux.dirs:
--------------------------------------------------------------------------------
1 | etc
2 | usr/bin
3 |
--------------------------------------------------------------------------------
/packaging/debian/miniflux.manpages:
--------------------------------------------------------------------------------
1 | miniflux.1
--------------------------------------------------------------------------------
/packaging/debian/miniflux.postinst:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | case "$1" in
6 | configure)
7 | adduser --system --disabled-password --disabled-login --home /var/empty \
8 | --no-create-home --quiet --force-badname --group miniflux
9 | ;;
10 | esac
11 |
12 | #DEBHELPER#
13 |
14 | exit 0
15 |
--------------------------------------------------------------------------------
/packaging/debian/rules:
--------------------------------------------------------------------------------
1 | #!/usr/bin/make -f
2 |
3 | DESTDIR=debian/miniflux
4 |
5 | %:
6 | dh $@ --with=systemd
7 |
8 | override_dh_auto_clean:
9 | override_dh_auto_test:
10 | override_dh_auto_build:
11 | override_dh_auto_install:
12 | cp miniflux.conf $(DESTDIR)/etc/miniflux.conf
13 | cp miniflux $(DESTDIR)/usr/bin/miniflux
14 |
15 | override_dh_installinit:
16 | dh_installinit --noscripts
17 |
--------------------------------------------------------------------------------
/packaging/docker/alpine/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM docker.io/library/golang:alpine3.20 AS build
2 | RUN apk add --no-cache build-base git make
3 | ADD . /go/src/app
4 | WORKDIR /go/src/app
5 | RUN make miniflux
6 |
7 | FROM docker.io/library/alpine:3.21
8 |
9 | LABEL org.opencontainers.image.title=Miniflux
10 | LABEL org.opencontainers.image.description="Miniflux is a minimalist and opinionated feed reader"
11 | LABEL org.opencontainers.image.vendor="Frédéric Guillot"
12 | LABEL org.opencontainers.image.licenses=Apache-2.0
13 | LABEL org.opencontainers.image.url=https://miniflux.app
14 | LABEL org.opencontainers.image.source=https://github.com/miniflux/v2
15 | LABEL org.opencontainers.image.documentation=https://miniflux.app/docs/
16 |
17 | EXPOSE 8080
18 | ENV LISTEN_ADDR=0.0.0.0:8080
19 | RUN apk --no-cache add ca-certificates tzdata
20 | COPY --from=build /go/src/app/miniflux /usr/bin/miniflux
21 | USER 65534
22 | CMD ["/usr/bin/miniflux"]
23 |
--------------------------------------------------------------------------------
/packaging/docker/distroless/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM docker.io/library/golang:bookworm AS build
2 | ADD . /go/src/app
3 | WORKDIR /go/src/app
4 | RUN make miniflux
5 |
6 | FROM gcr.io/distroless/base-debian12:nonroot
7 |
8 | LABEL org.opencontainers.image.title=Miniflux
9 | LABEL org.opencontainers.image.description="Miniflux is a minimalist and opinionated feed reader"
10 | LABEL org.opencontainers.image.vendor="Frédéric Guillot"
11 | LABEL org.opencontainers.image.licenses=Apache-2.0
12 | LABEL org.opencontainers.image.url=https://miniflux.app
13 | LABEL org.opencontainers.image.source=https://github.com/miniflux/v2
14 | LABEL org.opencontainers.image.documentation=https://miniflux.app/docs/
15 |
16 | EXPOSE 8080
17 | ENV LISTEN_ADDR=0.0.0.0:8080
18 | COPY --from=build /go/src/app/miniflux /usr/bin/miniflux
19 | CMD ["/usr/bin/miniflux"]
20 |
--------------------------------------------------------------------------------
/packaging/miniflux.conf:
--------------------------------------------------------------------------------
1 | # See https://miniflux.app/docs/configuration.html
2 |
3 | RUN_MIGRATIONS=1
4 |
--------------------------------------------------------------------------------
/packaging/rpm/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1 AS build
2 | ENV CGO_ENABLED=0
3 | ADD . /go/src/app
4 | WORKDIR /go/src/app
5 | RUN make miniflux
6 |
7 | FROM rockylinux:9
8 | RUN dnf install --setopt=install_weak_deps=False -y rpm-build systemd-rpm-macros
9 | RUN mkdir -p /root/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
10 | RUN echo "%_topdir /root/rpmbuild" >> .rpmmacros
11 | COPY --from=build /go/src/app/miniflux /root/rpmbuild/SOURCES/miniflux
12 | COPY --from=build /go/src/app/LICENSE /root/rpmbuild/SOURCES/
13 | COPY --from=build /go/src/app/ChangeLog /root/rpmbuild/SOURCES/
14 | COPY --from=build /go/src/app/miniflux.1 /root/rpmbuild/SOURCES/
15 | COPY --from=build /go/src/app/packaging/systemd/miniflux.service /root/rpmbuild/SOURCES/
16 | COPY --from=build /go/src/app/packaging/miniflux.conf /root/rpmbuild/SOURCES/
17 | COPY --from=build /go/src/app/packaging/rpm/miniflux.spec /root/rpmbuild/SPECS/miniflux.spec
18 |
--------------------------------------------------------------------------------