├── Procfile
├── packaging
├── debian
│ ├── compat
│ ├── miniflux.manpages
│ ├── miniflux.dirs
│ ├── copyright
│ ├── miniflux.postinst
│ ├── rules
│ ├── Dockerfile
│ ├── control
│ └── build.sh
├── miniflux.conf
├── rpm
│ └── Dockerfile
└── docker
│ ├── distroless
│ └── Dockerfile
│ └── alpine
│ └── Dockerfile
├── contrib
├── ansible
│ ├── roles
│ │ └── mgrote.miniflux
│ │ │ ├── defaults
│ │ │ └── main.yml
│ │ │ ├── handlers
│ │ │ └── main.yml
│ │ │ ├── templates
│ │ │ └── miniflux.conf
│ │ │ ├── README.md
│ │ │ └── tasks
│ │ │ └── main.yml
│ ├── playbooks
│ │ └── playbook.yml
│ └── inventories
│ │ └── group_vars
│ │ └── miniflux_vars.yml
├── grafana
│ └── README.md
├── docker-compose
│ ├── Caddyfile
│ ├── README.md
│ ├── basic.yml
│ └── caddy.yml
├── bruno
│ ├── miniflux
│ │ ├── bruno.json
│ │ ├── environments
│ │ │ └── Local.bru
│ │ ├── Get all users.bru
│ │ ├── Get current user.bru
│ │ ├── Get all categories.bru
│ │ ├── Get version and build information.bru
│ │ ├── Get a single user by ID.bru
│ │ ├── Discover feeds.bru
│ │ ├── Create a new category.bru
│ │ ├── Flush history.bru
│ │ ├── Create a feed.bru
│ │ ├── Get a single user by username.bru
│ │ ├── Get all feeds.bru
│ │ ├── Get all entries.bru
│ │ ├── Get feed counters.bru
│ │ ├── Refresh all feeds.bru
│ │ ├── Create a new user.bru
│ │ ├── Update entries status.bru
│ │ ├── OPML Export.bru
│ │ ├── Delete a user.bru
│ │ ├── Update a user.bru
│ │ ├── Update a feed.bru
│ │ ├── Delete a feed.bru
│ │ ├── Delete a category.bru
│ │ ├── Update a category.bru
│ │ ├── Get category feeds.bru
│ │ ├── Get a single feed.bru
│ │ ├── Get feed icon by icon ID.bru
│ │ ├── Mark feed as read.bru
│ │ ├── Save an entry.bru
│ │ ├── Get a single entry.bru
│ │ ├── Get category entries.bru
│ │ ├── Get feed entries.bru
│ │ ├── Get feed icon by feed ID.bru
│ │ ├── Refresh category feeds.bru
│ │ ├── Update entry.bru
│ │ ├── Bookmark an entry.bru
│ │ ├── Mark all user entries as read.bru
│ │ ├── Refresh a single feed.bru
│ │ ├── Get category entry.bru
│ │ ├── Fetch entry website content.bru
│ │ ├── Mark all category entries as read.bru
│ │ ├── Get a single feed entry.bru
│ │ └── OPML Import.bru
│ └── README.md
├── sysvinit
│ ├── README.md
│ └── etc
│ │ └── default
│ │ └── miniflux
├── README.md
└── thunder_client
│ └── README.md
├── internal
├── ui
│ ├── static
│ │ ├── js
│ │ │ ├── .jshintrc
│ │ │ └── request_builder.js
│ │ ├── bin
│ │ │ ├── favicon.ico
│ │ │ ├── icon-120.png
│ │ │ ├── icon-128.png
│ │ │ ├── icon-152.png
│ │ │ ├── icon-167.png
│ │ │ ├── icon-180.png
│ │ │ ├── icon-192.png
│ │ │ ├── icon-512.png
│ │ │ ├── favicon-16.png
│ │ │ ├── favicon-32.png
│ │ │ ├── maskable-icon-120.png
│ │ │ ├── maskable-icon-192.png
│ │ │ └── maskable-icon-512.png
│ │ └── css
│ │ │ ├── serif.css
│ │ │ └── sans_serif.css
│ ├── handler.go
│ ├── form
│ │ ├── webauthn.go
│ │ ├── category.go
│ │ ├── api_key.go
│ │ └── auth.go
│ ├── history_flush.go
│ ├── oauth2.go
│ ├── unread_mark_all_read.go
│ ├── offline.go
│ ├── entry_toggle_bookmark.go
│ ├── api_key_remove.go
│ ├── opml_export.go
│ ├── session_remove.go
│ ├── feed_remove.go
│ ├── static_stylesheet.go
│ ├── login_show.go
│ ├── static_favicon.go
│ ├── feed_mark_as_read.go
│ ├── opml_import.go
│ ├── category_create.go
│ ├── category_mark_as_read.go
│ ├── feed_icon.go
│ ├── api_key_create.go
│ ├── category_remove.go
│ ├── pagination.go
│ ├── api_key_list.go
│ ├── user_create.go
│ ├── entry_save.go
│ ├── entry_update_status.go
│ ├── feed_list.go
│ ├── logout.go
│ ├── static_app_icon.go
│ ├── category_list.go
│ ├── static_javascript.go
│ ├── user_list.go
│ ├── session_list.go
│ ├── user_remove.go
│ ├── about.go
│ ├── oauth2_redirect.go
│ ├── entry_enclosure_save_position.go
│ ├── subscription_add.go
│ ├── shared_entries.go
│ ├── category_feeds.go
│ ├── category_edit.go
│ └── user_edit.go
├── reader
│ ├── parser
│ │ ├── testdata
│ │ │ ├── encoding_ISO-8859-1.xml
│ │ │ ├── encoding_WINDOWS-1251.xml
│ │ │ └── no_encoding_ISO-8859-1.xml
│ │ ├── parser.go
│ │ └── format.go
│ ├── scraper
│ │ └── testdata
│ │ │ ├── img.html-result
│ │ │ ├── p.html-result
│ │ │ ├── iframe.html-result
│ │ │ ├── p.html
│ │ │ ├── img.html
│ │ │ └── iframe.html
│ ├── rss
│ │ ├── feedburner.go
│ │ └── parser.go
│ ├── sanitizer
│ │ ├── strip_tags_test.go
│ │ ├── truncate.go
│ │ └── strip_tags.go
│ ├── json
│ │ └── parser.go
│ ├── rdf
│ │ └── parser.go
│ ├── opml
│ │ ├── subscription.go
│ │ └── parser.go
│ ├── subscription
│ │ └── subscription.go
│ ├── dublincore
│ │ └── dublincore.go
│ ├── readingtime
│ │ └── readingtime.go
│ ├── encoding
│ │ └── encoding.go
│ └── atom
│ │ └── parser.go
├── config
│ └── config.go
├── version
│ └── version.go
├── model
│ ├── job.go
│ ├── categories_sort_options.go
│ ├── home_page.go
│ ├── model.go
│ ├── subscription.go
│ ├── api_key.go
│ ├── icon.go
│ ├── user_session.go
│ ├── category.go
│ ├── theme.go
│ ├── enclosure_test.go
│ ├── webauthn.go
│ └── enclosure.go
├── http
│ ├── request
│ │ ├── cookie.go
│ │ ├── cookie_test.go
│ │ └── client_ip.go
│ ├── route
│ │ └── route.go
│ ├── response
│ │ └── xml
│ │ │ └── xml.go
│ ├── server
│ │ └── middleware.go
│ └── cookie
│ │ └── cookie.go
├── cli
│ ├── flush_sessions.go
│ ├── info.go
│ ├── export_feeds.go
│ ├── ask_credentials.go
│ ├── health_check.go
│ ├── reset_password.go
│ ├── logger.go
│ └── create_admin.go
├── template
│ └── templates
│ │ ├── views
│ │ ├── feeds.html
│ │ ├── webauthn_rename.html
│ │ ├── create_api_key.html
│ │ ├── create_category.html
│ │ ├── import.html
│ │ ├── about.html
│ │ ├── bookmark_entries.html
│ │ ├── edit_category.html
│ │ ├── search_entries.html
│ │ ├── create_user.html
│ │ ├── sessions.html
│ │ └── category_feeds.html
│ │ ├── common
│ │ ├── feed_menu.html
│ │ ├── settings_menu.html
│ │ ├── entry_pagination.html
│ │ └── pagination.html
│ │ └── standalone
│ │ └── offline.html
├── oauth2
│ ├── profile.go
│ ├── provider.go
│ ├── manager.go
│ └── authorization.go
├── tests
│ ├── endpoint_test.go
│ ├── import_export_test.go
│ ├── version_test.go
│ └── subscription_test.go
├── validator
│ ├── subscription.go
│ ├── validator.go
│ └── category.go
├── locale
│ ├── locale_test.go
│ ├── locale.go
│ ├── plural_test.go
│ ├── catalog.go
│ └── error.go
├── worker
│ ├── pool.go
│ └── worker.go
├── api
│ ├── payload.go
│ ├── opml.go
│ └── icon.go
├── storage
│ ├── timezone.go
│ └── storage.go
├── timezone
│ └── timezone.go
├── integration
│ ├── rssbridge
│ │ └── rssbridge.go
│ └── matrixbot
│ │ └── matrixbot.go
└── crypto
│ └── crypto.go
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── bug_report.md
│ ├── feed-problems.md
│ ├── improvement.md
│ └── feature_request.md
├── pull_request_template.md
├── workflows
│ ├── build_binaries.yml
│ ├── linters.yml
│ ├── codeql-analysis.yml
│ ├── rpm_packages.yml
│ └── tests.yml
└── dependabot.yml
├── .gitignore
├── main.go
├── SECURITY.md
├── client
├── doc.go
└── README.md
└── .devcontainer
├── docker-compose.yml
└── devcontainer.json
/Procfile:
--------------------------------------------------------------------------------
1 | web: miniflux.app
2 |
--------------------------------------------------------------------------------
/packaging/debian/compat:
--------------------------------------------------------------------------------
1 | 10
2 |
--------------------------------------------------------------------------------
/packaging/debian/miniflux.manpages:
--------------------------------------------------------------------------------
1 | miniflux.1
--------------------------------------------------------------------------------
/packaging/debian/miniflux.dirs:
--------------------------------------------------------------------------------
1 | etc
2 | usr/bin
3 |
--------------------------------------------------------------------------------
/contrib/ansible/roles/mgrote.miniflux/defaults/main.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/contrib/grafana/README.md:
--------------------------------------------------------------------------------
1 | Grafana Dashboard for Miniflux
2 |
--------------------------------------------------------------------------------
/internal/ui/static/js/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esversion": 8
3 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | miniflux-*
2 | ./miniflux
3 | *.rpm
4 | *.deb
5 | .idea
6 | .vscode
--------------------------------------------------------------------------------
/contrib/docker-compose/Caddyfile:
--------------------------------------------------------------------------------
1 | miniflux.example.org
2 | reverse_proxy miniflux:8080
3 |
--------------------------------------------------------------------------------
/packaging/debian/copyright:
--------------------------------------------------------------------------------
1 | Files: *
2 | Copyright: 2017-2023 Frederic Guillot
3 | License: Apache
--------------------------------------------------------------------------------
/packaging/miniflux.conf:
--------------------------------------------------------------------------------
1 | # See https://miniflux.app/docs/configuration.html
2 |
3 | RUN_MIGRATIONS=1
4 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/bruno.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1",
3 | "name": "Miniflux",
4 | "type": "collection"
5 | }
--------------------------------------------------------------------------------
/internal/ui/static/bin/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/ui/static/bin/favicon.ico
--------------------------------------------------------------------------------
/internal/ui/static/bin/icon-120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/ui/static/bin/icon-120.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/ui/static/bin/icon-128.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/icon-152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/ui/static/bin/icon-152.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/icon-167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/ui/static/bin/icon-167.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/icon-180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/ui/static/bin/icon-180.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/ui/static/bin/icon-192.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/ui/static/bin/icon-512.png
--------------------------------------------------------------------------------
/contrib/ansible/playbooks/playbook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - hosts: miniflux
3 | roles:
4 | - { role: mgrote.miniflux, tags: "miniflux" }
--------------------------------------------------------------------------------
/internal/ui/static/bin/favicon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/ui/static/bin/favicon-16.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/favicon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/ui/static/bin/favicon-32.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/maskable-icon-120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/ui/static/bin/maskable-icon-120.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/maskable-icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/ui/static/bin/maskable-icon-192.png
--------------------------------------------------------------------------------
/internal/ui/static/bin/maskable-icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/ui/static/bin/maskable-icon-512.png
--------------------------------------------------------------------------------
/internal/reader/parser/testdata/encoding_ISO-8859-1.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/reader/parser/testdata/encoding_ISO-8859-1.xml
--------------------------------------------------------------------------------
/internal/reader/parser/testdata/encoding_WINDOWS-1251.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/reader/parser/testdata/encoding_WINDOWS-1251.xml
--------------------------------------------------------------------------------
/internal/reader/parser/testdata/no_encoding_ISO-8859-1.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RaymiiOrg/miniflux-v2/main/internal/reader/parser/testdata/no_encoding_ISO-8859-1.xml
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | Do you follow the guidelines?
2 |
3 | - [ ] I have tested my changes
4 | - [ ] I read this document: https://miniflux.app/faq.html#pull-request
5 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/environments/Local.bru:
--------------------------------------------------------------------------------
1 | vars {
2 | minifluxBaseURL: http://127.0.0.1:8080
3 | minifluxUsername: admin
4 | }
5 | vars:secret [
6 | minifluxPassword
7 | ]
8 |
--------------------------------------------------------------------------------
/contrib/sysvinit/README.md:
--------------------------------------------------------------------------------
1 |
2 | System-V init for e.g. http://devuan.org
3 |
4 | Assumes an executable `/usr/local/bin/miniflux`.
5 |
6 | Configure in `etc/default/miniflux`
7 |
8 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a bug report
4 | title: ''
5 | labels: bug, triage needed
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/internal/reader/scraper/testdata/img.html-result:
--------------------------------------------------------------------------------
1 | 



2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feed-problems.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feed Problems
3 | about: Problems with a feed or a website
4 | title: ''
5 | labels: feed problems, triage needed
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/improvement.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Improvement
3 | about: Do you have an idea to improve an existing feature?
4 | title: ''
5 | labels: improvements
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/contrib/bruno/README.md:
--------------------------------------------------------------------------------
1 | This folder contains Miniflux API collection for [Bruno](https://www.usebruno.com).
2 |
3 | Bruno is a lightweight alternative to Postman/Insomnia.
4 |
5 | - https://www.usebruno.com
6 | - https://github.com/usebruno/bruno
--------------------------------------------------------------------------------
/contrib/README.md:
--------------------------------------------------------------------------------
1 | The contrib directory contains various useful things contributed by the community.
2 |
3 | Community contributions are not officially supported by the maintainers.
4 | There is no guarantee whatsoever that anything in this folder works.
5 |
--------------------------------------------------------------------------------
/internal/reader/scraper/testdata/iframe.html-result:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: feature request
6 | assignees: ''
7 |
8 | ---
9 |
10 | - [ ] I have read this document: https://miniflux.app/opinionated.html#feature-request
11 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/contrib/ansible/roles/mgrote.miniflux/handlers/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: start_miniflux.service
3 | become: yes
4 | systemd:
5 | name: miniflux
6 | state: restarted
7 | enabled: yes
8 | # wait 15 seconds(for systemd)
9 | - name: miniflux_wait
10 | wait_for:
11 | timeout: 15
12 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get all users.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get all users
3 | type: http
4 | seq: 2
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/users
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get current user.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get current user
3 | type: http
4 | seq: 1
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/me
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get all categories.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get all categories
3 | type: http
4 | seq: 9
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/categories
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/contrib/ansible/inventories/group_vars/miniflux_vars.yml:
--------------------------------------------------------------------------------
1 | ---
2 | miniflux_linux_user: miniflux
3 | miniflux_db_user_name: miniflux_db_user
4 | miniflux_db_user_password: miniflux_db_user_password
5 | miniflux_db: miniflux_db
6 | miniflux_admin_name: admin
7 | miniflux_admin_passwort: miniflux_admin_password
8 | miniflux_port: 8080
9 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/internal/reader/scraper/testdata/img.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get version and build information.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get version and build information
3 | type: http
4 | seq: 42
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/version
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/contrib/docker-compose/README.md:
--------------------------------------------------------------------------------
1 | Docker-Compose Examples
2 | =======================
3 |
4 | Here are few Docker Compose examples:
5 |
6 | - `basic.yml`: Basic example
7 | - `caddy.yml`: Use Caddy as reverse-proxy with automatic HTTPS
8 | - `traefik.yml`: Use Traefik as reverse-proxy with automatic HTTPS
9 |
10 | ```bash
11 | docker compose -f basic.yml up -d
12 | ```
13 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/contrib/sysvinit/etc/default/miniflux:
--------------------------------------------------------------------------------
1 | # sourced by /etc/init.d/miniflux
2 | # see cluster port in pg_lsclusters and ls -Al /var/run/postgresql/
3 | export DATABASE_URL='host=/var/run/postgresql/ port=5433 user=miniflux password= dbname=miniflux sslmode=disable'
4 | export LISTEN_ADDR='127.0.0.1:8081'
5 | export BASE_URL='https:// and path/'
6 |
7 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get a single user by ID.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get a single user by ID
3 | type: http
4 | seq: 3
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/users/{{userID}}
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | vars:pre-request {
19 | userID: 1
20 | }
21 |
--------------------------------------------------------------------------------
/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/debian/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG BASE_IMAGE_ARCH="amd64"
2 |
3 | FROM ${BASE_IMAGE_ARCH}/golang:bookworm AS build
4 |
5 | ENV DEBIAN_FRONTEND=noninteractive
6 | ENV CGO_ENABLED=0
7 |
8 | RUN apt-get update -q && \
9 | apt-get install -y -qq build-essential devscripts dh-make debhelper && \
10 | mkdir -p /build/debian
11 |
12 | ADD . /src
13 |
14 | CMD ["/src/packaging/debian/build.sh"]
15 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Discover feeds.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Discover feeds
3 | type: http
4 | seq: 18
5 | }
6 |
7 | post {
8 | url: {{minifluxBaseURL}}/v1/discover
9 | body: json
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "url": "https://miniflux.app"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/internal/reader/scraper/testdata/iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Create a new category.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Create a new category
3 | type: http
4 | seq: 10
5 | }
6 |
7 | post {
8 | url: {{minifluxBaseURL}}/v1/categories
9 | body: json
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "title": "Test"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Flush history.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Flush history
3 | type: http
4 | seq: 40
5 | }
6 |
7 | put {
8 | url: {{minifluxBaseURL}}/v1/flush-history
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "url": "https://miniflux.app"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Create a feed.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Create a feed
3 | type: http
4 | seq: 19
5 | }
6 |
7 | post {
8 | url: {{minifluxBaseURL}}/v1/feeds
9 | body: json
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "feed_url": "https://miniflux.app/feed.xml"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get a single user by username.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get a single user by username
3 | type: http
4 | seq: 4
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/users/{{username}}
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | vars:pre-request {
19 | username: admin
20 | }
21 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get all feeds.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get all feeds
3 | type: http
4 | seq: 20
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/feeds
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "feed_url": "https://miniflux.app/feed.xml"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get all entries.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get all entries
3 | type: http
4 | seq: 34
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/entries
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "feed_url": "https://miniflux.app/feed.xml"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get feed counters.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get feed counters
3 | type: http
4 | seq: 21
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/feeds/counters
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "feed_url": "https://miniflux.app/feed.xml"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Refresh all feeds.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Refresh all feeds
3 | type: http
4 | seq: 22
5 | }
6 |
7 | put {
8 | url: {{minifluxBaseURL}}/v1/feeds/refresh
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "feed_url": "https://miniflux.app/feed.xml"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Create a new user.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Create a new user
3 | type: http
4 | seq: 5
5 | }
6 |
7 | post {
8 | url: {{minifluxBaseURL}}/v1/users
9 | body: json
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "username": "foobar",
21 | "password": "secret123"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Update entries status.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Update entries status
3 | type: http
4 | seq: 35
5 | }
6 |
7 | put {
8 | url: {{minifluxBaseURL}}/v1/entries
9 | body: json
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "entry_ids": [1698, 1699],
21 | "status": "read"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/OPML Export.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: OPML Export
3 | type: http
4 | seq: 30
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/export
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "user_agent": "My user agent"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | feedID: 19
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Delete a user.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Delete a user
3 | type: http
4 | seq: 7
5 | }
6 |
7 | delete {
8 | url: {{minifluxBaseURL}}/v1/users/{{userID}}
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "language": "fr_FR"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | userID: 2
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Update a user.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Update a user
3 | type: http
4 | seq: 6
5 | }
6 |
7 | put {
8 | url: {{minifluxBaseURL}}/v1/users/{{userID}}
9 | body: json
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "language": "fr_FR"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | userID: 1
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Update a feed.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Update a feed
3 | type: http
4 | seq: 25
5 | }
6 |
7 | put {
8 | url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}
9 | body: json
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "user_agent": "My user agent"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | feedID: 18
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Delete a feed.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Delete a feed
3 | type: http
4 | seq: 26
5 | }
6 |
7 | delete {
8 | url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "user_agent": "My user agent"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | feedID: 18
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Delete a category.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Delete a category
3 | type: http
4 | seq: 12
5 | }
6 |
7 | delete {
8 | url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "title": "Test Update"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | categoryID: 1
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Update a category.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Update a category
3 | type: http
4 | seq: 11
5 | }
6 |
7 | put {
8 | url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}
9 | body: json
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "title": "Test Update"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | categoryID: 1
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get category feeds.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get category feeds
3 | type: http
4 | seq: 14
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}/feeds
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "title": "Test Update"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | categoryID: 2
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get a single feed.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get a single feed
3 | type: http
4 | seq: 24
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "feed_url": "https://miniflux.app/feed.xml"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | feedID: 18
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get feed icon by icon ID.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get feed icon by icon ID
3 | type: http
4 | seq: 28
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/icons/{{iconID}}
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "user_agent": "My user agent"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | iconID: 11
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Mark feed as read.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Mark feed as read
3 | type: http
4 | seq: 29
5 | }
6 |
7 | put {
8 | url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/mark-all-as-read
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "user_agent": "My user agent"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | feedID: 19
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Save an entry.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Save an entry
3 | type: http
4 | seq: 38
5 | }
6 |
7 | post {
8 | url: {{minifluxBaseURL}}/v1/entries/{{entryID}}/save
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "feed_url": "https://miniflux.app/feed.xml"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | entryID: 1698
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get a single entry.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get a single entry
3 | type: http
4 | seq: 36
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/entries/{{entryID}}
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "feed_url": "https://miniflux.app/feed.xml"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | entryID: 1698
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get category entries.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get category entries
3 | type: http
4 | seq: 16
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}/entries
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "title": "Test Update"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | categoryID: 2
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get feed entries.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get feed entries
3 | type: http
4 | seq: 32
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/entries
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "feed_url": "https://miniflux.app/feed.xml"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | feedID: 19
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get feed icon by feed ID.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get feed icon by feed ID
3 | type: http
4 | seq: 27
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/icon
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "user_agent": "My user agent"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | feedID: 19
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Refresh category feeds.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Refresh category feeds
3 | type: http
4 | seq: 15
5 | }
6 |
7 | put {
8 | url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}/refresh
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "title": "Test Update"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | categoryID: 2
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Update entry.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Update entry
3 | type: http
4 | seq: 41
5 | }
6 |
7 | put {
8 | url: {{minifluxBaseURL}}/v1/entries/{{entryID}}
9 | body: json
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "title": "New title",
21 | "content": "Some text"
22 | }
23 | }
24 |
25 | vars:pre-request {
26 | entryID: 1789
27 | }
28 |
--------------------------------------------------------------------------------
/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 == http.ErrNoCookie {
12 | return ""
13 | }
14 |
15 | return cookie.Value
16 | }
17 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Bookmark an entry.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Bookmark an entry
3 | type: http
4 | seq: 37
5 | }
6 |
7 | put {
8 | url: {{minifluxBaseURL}}/v1/entries/{{entryID}}/bookmark
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "feed_url": "https://miniflux.app/feed.xml"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | entryID: 1698
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Mark all user entries as read.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Mark all user entries as read
3 | type: http
4 | seq: 8
5 | }
6 |
7 | put {
8 | url: {{minifluxBaseURL}}/v1/users/{{userID}}/mark-all-as-read
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "title": "Test Update"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | userID: 1
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Refresh a single feed.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Refresh a single feed
3 | type: http
4 | seq: 23
5 | }
6 |
7 | put {
8 | url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/refresh
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "feed_url": "https://miniflux.app/feed.xml"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | feedID: 18
26 | }
27 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get category entry.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get category entry
3 | type: http
4 | seq: 17
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}/entries/{{entryID}}
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "title": "Test Update"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | categoryID: 2
26 | entryID: 1
27 | }
28 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Fetch entry website content.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Fetch entry website content
3 | type: http
4 | seq: 39
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/entries/{{entryID}}/fetch-content
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "feed_url": "https://miniflux.app/feed.xml"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | entryID: 1698
26 | }
27 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Mark all category entries as read.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Mark all category entries as read
3 | type: http
4 | seq: 13
5 | }
6 |
7 | put {
8 | url: {{minifluxBaseURL}}/v1/categories/{{categoryID}}/mark-all-as-read
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "title": "Test Update"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | categoryID: 2
26 | }
27 |
--------------------------------------------------------------------------------
/internal/template/templates/views/feeds.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.feeds.title" }} ({{ .total }}){{ end }}
2 |
3 | {{ define "content"}}
4 |
8 |
9 | {{ if not .feeds }}
10 | {{ t "alert.no_feed" }}
11 | {{ else }}
12 | {{ template "feed_list" dict "user" .user "feeds" .feeds "ParsingErrorCount" .ParsingErrorCount }}
13 | {{ end }}
14 |
15 | {{ end }}
16 |
--------------------------------------------------------------------------------
/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/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 | // FeedBurnerElement represents FeedBurner XML elements.
7 | type FeedBurnerElement 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/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 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/Get a single feed entry.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get a single feed entry
3 | type: http
4 | seq: 33
5 | }
6 |
7 | get {
8 | url: {{minifluxBaseURL}}/v1/feeds/{{feedID}}/entries/{{entryID}}
9 | body: none
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "feed_url": "https://miniflux.app/feed.xml"
21 | }
22 | }
23 |
24 | vars:pre-request {
25 | feedID: 19
26 | entryID: 1698
27 | }
28 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Only the latest stable version is supported.
6 |
7 | ## Reporting a Vulnerability
8 |
9 | Preferably, [report the vulnerability privately using GitHub](https://github.com/miniflux/v2/security/advisories/new) ([documentation](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability)).
10 |
11 | If you do not want to use GitHub, send an email to `security AT miniflux DOT net` with all the steps to reproduce the problem.
12 |
--------------------------------------------------------------------------------
/internal/tests/endpoint_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | //go:build integration
5 | // +build integration
6 |
7 | package tests
8 |
9 | import (
10 | "testing"
11 |
12 | miniflux "miniflux.app/v2/client"
13 | )
14 |
15 | func TestWithBadEndpoint(t *testing.T) {
16 | client := miniflux.New("bad url", testAdminUsername, testAdminPassword)
17 | _, err := client.Users()
18 | if err == nil {
19 | t.Fatal(`Using a bad URL should raise an error`)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/contrib/ansible/roles/mgrote.miniflux/templates/miniflux.conf:
--------------------------------------------------------------------------------
1 | # See https://docs.miniflux.app/
2 |
3 | LISTEN_ADDR=0.0.0.0:{{ miniflux_port }}
4 | DATABASE_URL=user={{ miniflux_db_user_name }} password={{ miniflux_db_user_password }} dbname={{ miniflux_db }} sslmode=disable
5 |
6 | POLLING_FREQUENCY=15
7 | PROXY_IMAGES=http-only
8 |
9 | # Run SQL migrations automatically:
10 | RUN_MIGRATIONS=1
11 |
12 | CREATE_ADMIN=1
13 | ADMIN_USERNAME={{ miniflux_admin_name }}
14 | ADMIN_PASSWORD={{ miniflux_admin_passwort }}
15 |
16 | POLLING_FREQUENCY=10
17 |
18 | # Options: https://miniflux.app/miniflux.1.html
19 |
--------------------------------------------------------------------------------
/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/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/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/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/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 string
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"),
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/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/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 | return nil
18 | }
19 |
--------------------------------------------------------------------------------
/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/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/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 | text = strings.ReplaceAll(text, "\n", " ")
11 | text = strings.ReplaceAll(text, "\t", " ")
12 | text = strings.ReplaceAll(text, " ", " ")
13 | text = strings.TrimSpace(text)
14 |
15 | // Convert to runes to be safe with unicode
16 | runes := []rune(text)
17 | if len(runes) > max {
18 | return strings.TrimSpace(string(runes[:max])) + "…"
19 | }
20 |
21 | return text
22 | }
23 |
--------------------------------------------------------------------------------
/contrib/ansible/roles/mgrote.miniflux/README.md:
--------------------------------------------------------------------------------
1 | ## mgrote.miniflux
2 |
3 | ### Details
4 | Installs and configures Miniflux v2 with ansible
5 |
6 | ### Works on...
7 | - [x] Ubuntu (>=18.04)
8 |
9 | ### Variables and Defaults
10 | ##### Linux User
11 | miniflux_linux_user: miniflux
12 | ##### DB User
13 | miniflux_db_user_name: miniflux_db_user
14 | ##### DB Password
15 | miniflux_db_user_password: qqqqqqqqqqqqq
16 | ##### Database
17 | miniflux_db: miniflux_db
18 | ##### Username Miniflux Admin
19 | miniflux_admin_name: admin
20 | ##### Password Miniflux Admin
21 | miniflux_admin_passwort: hallowelt
22 | ##### Port for Miniflux Frontend
23 | miniflux_port: 8080
24 |
--------------------------------------------------------------------------------
/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 | feed := new(jsonFeed)
17 | if err := json.NewDecoder(data).Decode(&feed); err != nil {
18 | return nil, fmt.Errorf("json: unable to parse feed: %w", err)
19 | }
20 |
21 | return feed.Transform(baseURL), nil
22 | }
23 |
--------------------------------------------------------------------------------
/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.Reader) (*model.Feed, error) {
16 | feed := new(rssFeed)
17 | if err := xml.NewDecoder(data).Decode(feed); err != nil {
18 | return nil, fmt.Errorf("rss: unable to parse feed: %w", err)
19 | }
20 | return feed.Transform(baseURL), nil
21 | }
22 |
--------------------------------------------------------------------------------
/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.Reader) (*model.Feed, error) {
16 | feed := new(rdfFeed)
17 | if err := xml.NewDecoder(data).Decode(feed); err != nil {
18 | return nil, fmt.Errorf("rdf: unable to parse feed: %w", err)
19 | }
20 |
21 | return feed.Transform(baseURL), nil
22 | }
23 |
--------------------------------------------------------------------------------
/internal/template/templates/common/feed_menu.html:
--------------------------------------------------------------------------------
1 | {{ define "feed_menu" }}
2 |
19 | {{ end }}
20 |
--------------------------------------------------------------------------------
/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) removeAPIKey(w http.ResponseWriter, r *http.Request) {
15 | keyID := request.RouteInt64Param(r, "keyID")
16 | err := h.store.RemoveAPIKey(request.UserID(r), keyID)
17 | if err != nil {
18 | html.ServerError(w, r, err)
19 | return
20 | }
21 |
22 | html.Redirect(w, r, route.Path(h.router, "apiKeys"))
23 | }
24 |
--------------------------------------------------------------------------------
/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 | // OptionalString populates an optional string field.
7 | func OptionalString(value string) *string {
8 | if value != "" {
9 | return &value
10 | }
11 | return nil
12 | }
13 |
14 | // OptionalInt populates an optional int field.
15 | func OptionalInt(value int) *int {
16 | if value > 0 {
17 | return &value
18 | }
19 | return nil
20 | }
21 |
22 | // OptionalInt64 populates an optional int64 field.
23 | func OptionalInt64(value int64) *int64 {
24 | if value > 0 {
25 | return &value
26 | }
27 | return nil
28 | }
29 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/workflows/build_binaries.yml:
--------------------------------------------------------------------------------
1 | name: Build Binaries
2 | on:
3 | workflow_dispatch:
4 | push:
5 | tags:
6 | - '[0-9]+.[0-9]+.[0-9]+'
7 | jobs:
8 | build:
9 | name: Build
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Set up Golang
13 | uses: actions/setup-go@v4
14 | with:
15 | go-version: "1.21"
16 | - name: Checkout
17 | uses: actions/checkout@v4
18 | - name: Compile binaries
19 | run: make build
20 | - name: Upload binaries
21 | uses: actions/upload-artifact@v3
22 | with:
23 | name: miniflux-linux-amd64
24 | path: miniflux-linux-amd64
25 | if-no-files-found: error
26 | retention-days: 5
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/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/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/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 | FetchViaProxy bool `json:"fetch_via_proxy"`
14 | AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
15 | }
16 |
--------------------------------------------------------------------------------
/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 | }
13 |
14 | // Equals compare two subscriptions.
15 | func (s Subcription) Equals(subscription *Subcription) bool {
16 | return s.Title == subscription.Title && s.SiteURL == subscription.SiteURL &&
17 | s.FeedURL == subscription.FeedURL && s.CategoryName == subscription.CategoryName
18 | }
19 |
20 | // SubcriptionList is a list of subscriptions.
21 | type SubcriptionList []*Subcription
22 |
--------------------------------------------------------------------------------
/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/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="%s", URL="%s", Type="%s"`, s.Title, s.URL, s.Type)
21 | }
22 |
23 | // Subscriptions represents a list of subscription.
24 | type Subscriptions []*Subscription
25 |
--------------------------------------------------------------------------------
/client/doc.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | /*
5 | Package client implements a client library for the Miniflux REST API.
6 |
7 | # Examples
8 |
9 | This code snippet fetch the list of users:
10 |
11 | import (
12 | miniflux "miniflux.app/v2/client"
13 | )
14 |
15 | client := miniflux.New("https://api.example.org", "admin", "secret")
16 | users, err := client.Users()
17 | if err != nil {
18 | fmt.Println(err)
19 | return
20 | }
21 | fmt.Println(users, err)
22 |
23 | This one discover subscriptions on a website:
24 |
25 | subscriptions, err := client.Discover("https://example.org/")
26 | if err != nil {
27 | fmt.Println(err)
28 | return
29 | }
30 | fmt.Println(subscriptions)
31 | */
32 | package client // import "miniflux.app/v2/client"
33 |
--------------------------------------------------------------------------------
/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 |
9 | "miniflux.app/v2/internal/locale"
10 | )
11 |
12 | // APIKeyForm represents the API Key form.
13 | type APIKeyForm struct {
14 | Description string
15 | }
16 |
17 | // Validate makes sure the form values are valid.
18 | func (a APIKeyForm) Validate() *locale.LocalizedError {
19 | if a.Description == "" {
20 | return locale.NewLocalizedError("error.fields_mandatory")
21 | }
22 |
23 | return nil
24 | }
25 |
26 | // NewAPIKeyForm returns a new APIKeyForm.
27 | func NewAPIKeyForm(r *http.Request) *APIKeyForm {
28 | return &APIKeyForm{
29 | Description: r.FormValue("description"),
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/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 -y rpm-build systemd
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 |
--------------------------------------------------------------------------------
/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 returns the list of available languages.
7 | func AvailableLanguages() map[string]string {
8 | return map[string]string{
9 | "en_US": "English",
10 | "es_ES": "Español",
11 | "fr_FR": "Français",
12 | "de_DE": "Deutsch",
13 | "pl_PL": "Polski",
14 | "pt_BR": "Português Brasileiro",
15 | "zh_CN": "简体中文",
16 | "zh_TW": "繁體中文",
17 | "nl_NL": "Nederlands",
18 | "ru_RU": "Русский",
19 | "it_IT": "Italiano",
20 | "ja_JP": "日本語",
21 | "tr_TR": "Türkçe",
22 | "el_EL": "Ελληνικά",
23 | "fi_FI": "Suomi",
24 | "hi_IN": "हिन्दी",
25 | "uk_UA": "Українська",
26 | "id_ID": "Bahasa Indonesia",
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.devcontainer/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 | services:
3 | app:
4 | image: mcr.microsoft.com/devcontainers/go
5 | volumes:
6 | - ..:/workspace:cached
7 | command: sleep infinity
8 | network_mode: service:db
9 | environment:
10 | - CREATE_ADMIN=1
11 | - ADMIN_USERNAME=admin
12 | - ADMIN_PASSWORD=test123
13 | db:
14 | image: postgres:15
15 | restart: unless-stopped
16 | volumes:
17 | - postgres-data:/var/lib/postgresql/data
18 | hostname: postgres
19 | environment:
20 | POSTGRES_DB: miniflux2
21 | POSTGRES_USER: postgres
22 | POSTGRES_PASSWORD: postgres
23 | POSTGRES_HOST_AUTH_METHOD: trust
24 | ports:
25 | - 5432:5432
26 | apprise:
27 | image: caronc/apprise:latest
28 | restart: unless-stopped
29 | hostname: apprise
30 | volumes:
31 | postgres-data: null
32 |
--------------------------------------------------------------------------------
/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 | "miniflux.app/v2/internal/crypto"
10 | )
11 |
12 | // APIKey represents an application API key.
13 | type APIKey struct {
14 | ID int64
15 | UserID int64
16 | Token string
17 | Description string
18 | LastUsedAt *time.Time
19 | CreatedAt time.Time
20 | }
21 |
22 | // NewAPIKey initializes a new APIKey.
23 | func NewAPIKey(userID int64, description string) *APIKey {
24 | return &APIKey{
25 | UserID: userID,
26 | Token: crypto.GenerateRandomString(32),
27 | Description: description,
28 | }
29 | }
30 |
31 | // APIKeys represents a collection of API Key.
32 | type APIKeys []*APIKey
33 |
--------------------------------------------------------------------------------
/contrib/bruno/miniflux/OPML Import.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: OPML Import
3 | type: http
4 | seq: 31
5 | }
6 |
7 | post {
8 | url: {{minifluxBaseURL}}/v1/import
9 | body: xml
10 | auth: basic
11 | }
12 |
13 | auth:basic {
14 | username: {{minifluxUsername}}
15 | password: {{minifluxPassword}}
16 | }
17 |
18 | body:json {
19 | {
20 | "user_agent": "My user agent"
21 | }
22 | }
23 |
24 | body:xml {
25 |
26 |
27 |
28 | Miniflux
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | }
37 |
38 | vars:pre-request {
39 | feedID: 19
40 | }
41 |
--------------------------------------------------------------------------------
/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 | "bytes"
8 | "io"
9 |
10 | "golang.org/x/net/html"
11 | )
12 |
13 | // StripTags removes all HTML/XML tags from the input string.
14 | func StripTags(input string) string {
15 | tokenizer := html.NewTokenizer(bytes.NewBufferString(input))
16 | var buffer bytes.Buffer
17 |
18 | for {
19 | if tokenizer.Next() == html.ErrorToken {
20 | err := tokenizer.Err()
21 | if err == io.EOF {
22 | return buffer.String()
23 | }
24 |
25 | return ""
26 | }
27 |
28 | token := tokenizer.Token()
29 | switch token.Type {
30 | case html.TextToken:
31 | buffer.WriteString(token.Data)
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Miniflux",
3 | "dockerComposeFile": "docker-compose.yml",
4 | "service": "app",
5 | "workspaceFolder": "/workspace",
6 | "remoteUser": "vscode",
7 | "forwardPorts": [
8 | 8080
9 | ],
10 | "features": {
11 | "ghcr.io/devcontainers/features/github-cli:1": {},
12 | "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
13 | },
14 | "customizations": {
15 | "vscode": {
16 | "settings": {
17 | "go.toolsManagement.checkForUpdates": "local",
18 | "go.useLanguageServer": true,
19 | "go.gopath": "/go"
20 | },
21 | "extensions": [
22 | "ms-azuretools.vscode-docker",
23 | "golang.go",
24 | "rangav.vscode-thunder-client",
25 | "GitHub.codespaces",
26 | "GitHub.copilot",
27 | "GitHub.copilot-chat"
28 | ]
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/internal/template/templates/common/settings_menu.html:
--------------------------------------------------------------------------------
1 | {{ define "settings_menu" }}
2 |
24 | {{ end }}
--------------------------------------------------------------------------------
/.github/workflows/linters.yml:
--------------------------------------------------------------------------------
1 | name: Linters
2 | permissions: read-all
3 |
4 | on:
5 | pull_request:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | jshint:
11 | name: Javascript Linter
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Install jshint
16 | run: |
17 | sudo npm install -g jshint@2.13.3
18 | - name: Run jshint
19 | run: jshint ui/static/js/*.js
20 |
21 | golangci:
22 | name: Golang Linter
23 | runs-on: ubuntu-latest
24 | steps:
25 | - uses: actions/checkout@v4
26 | - uses: actions/setup-go@v4
27 | with:
28 | go-version: "1.21"
29 | - uses: golangci/golangci-lint-action@v3
30 | with:
31 | args: --timeout 10m --skip-dirs tests --disable errcheck --enable sqlclosecheck --enable misspell --enable gofmt --enable goimports --enable whitespace
32 |
--------------------------------------------------------------------------------
/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/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/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 a 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/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 | }
18 |
19 | // DataURL returns the data URL of the icon.
20 | func (i *Icon) DataURL() string {
21 | return fmt.Sprintf("%s;base64,%s", i.MimeType, base64.StdEncoding.EncodeToString(i.Content))
22 | }
23 |
24 | // Icons represents a list of icons.
25 | type Icons []*Icon
26 |
27 | // FeedIcon is a junction table between feeds and icons.
28 | type FeedIcon struct {
29 | FeedID int64 `json:"feed_id"`
30 | IconID int64 `json:"icon_id"`
31 | }
32 |
--------------------------------------------------------------------------------
/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 := 0; i < nbWorkers; i++ {
30 | worker := &Worker{id: i, store: store}
31 | go worker.Run(workerPool.queue)
32 | }
33 |
34 | return workerPool
35 | }
36 |
--------------------------------------------------------------------------------
/internal/template/templates/common/entry_pagination.html:
--------------------------------------------------------------------------------
1 | {{ define "entry_pagination" }}
2 |
19 | {{ end }}
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | permissions: read-all
4 |
5 | on:
6 | push:
7 | branches: [ main ]
8 | pull_request:
9 | # The branches below must be a subset of the branches above
10 | branches: [ main ]
11 | schedule:
12 | - cron: '45 22 * * 3'
13 |
14 | jobs:
15 | analyze:
16 | name: Analyze
17 | runs-on: ubuntu-latest
18 | permissions:
19 | actions: read
20 | contents: read
21 | security-events: write
22 |
23 | strategy:
24 | fail-fast: false
25 | matrix:
26 | language: [ 'go', 'javascript' ]
27 |
28 | steps:
29 | - name: Checkout repository
30 | uses: actions/checkout@v4
31 |
32 | - name: Initialize CodeQL
33 | uses: github/codeql-action/init@v2
34 |
35 | - name: Autobuild
36 | uses: github/codeql-action/autobuild@v2
37 |
38 | - name: Perform CodeQL Analysis
39 | uses: github/codeql-action/analyze@v2
40 |
--------------------------------------------------------------------------------
/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/template/templates/views/webauthn_rename.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.webauthn_rename.title" }}{{ end }}
2 |
3 | {{ define "content"}}
4 |
7 |
8 |
22 | {{ end }}
23 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/template/templates/common/pagination.html:
--------------------------------------------------------------------------------
1 | {{ define "pagination" }}
2 |
19 | {{ end }}
20 |
--------------------------------------------------------------------------------
/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/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_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/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) showIcon(w http.ResponseWriter, r *http.Request) {
16 | iconID := request.RouteInt64Param(r, "iconID")
17 | icon, err := h.store.IconByID(iconID)
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", `default-src 'self'`)
30 | b.WithHeader("Content-Type", icon.MimeType)
31 | b.WithBody(icon.Content)
32 | b.WithoutCompression()
33 | b.Write()
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/contrib/docker-compose/basic.yml:
--------------------------------------------------------------------------------
1 | services:
2 | miniflux:
3 | image: ${MINIFLUX_IMAGE:-miniflux/miniflux:latest}
4 | container_name: miniflux
5 | restart: always
6 | ports:
7 | - "80:8080"
8 | depends_on:
9 | db:
10 | condition: service_healthy
11 | environment:
12 | - DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
13 | - RUN_MIGRATIONS=1
14 | - CREATE_ADMIN=1
15 | - ADMIN_USERNAME=admin
16 | - ADMIN_PASSWORD=test123
17 | - DEBUG=1
18 | # Optional health check:
19 | # healthcheck:
20 | # test: ["CMD", "/usr/bin/miniflux", "-healthcheck", "auto"]
21 | db:
22 | image: postgres:15
23 | container_name: postgres
24 | environment:
25 | - POSTGRES_USER=miniflux
26 | - POSTGRES_PASSWORD=secret
27 | volumes:
28 | - miniflux-db:/var/lib/postgresql/data
29 | healthcheck:
30 | test: ["CMD", "pg_isready", "-U", "miniflux"]
31 | interval: 10s
32 | start_period: 30s
33 | volumes:
34 | miniflux-db:
35 |
--------------------------------------------------------------------------------
/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/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/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/template/templates/views/create_api_key.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.new_api_key.title" }}{{ end }}
2 |
3 | {{ define "content"}}
4 |
8 |
9 |
23 | {{ end }}
24 |
--------------------------------------------------------------------------------
/.github/workflows/rpm_packages.yml:
--------------------------------------------------------------------------------
1 | name: RPM Packages
2 | permissions: read-all
3 | on:
4 | push:
5 | tags:
6 | - '[0-9]+.[0-9]+.[0-9]+'
7 | pull_request:
8 | branches: [ main ]
9 | jobs:
10 | test-package:
11 | if: github.event.pull_request
12 | name: Test Packages
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | with:
17 | fetch-depth: 0
18 | - name: Build RPM Package
19 | run: make rpm
20 | - name: List generated files
21 | run: ls -l *.rpm
22 | publish-package:
23 | if: ${{ ! github.event.pull_request }}
24 | name: Publish Packages
25 | runs-on: ubuntu-latest
26 | steps:
27 | - uses: actions/checkout@v4
28 | with:
29 | fetch-depth: 0
30 | - name: Build RPM Package
31 | run: make rpm
32 | - name: List generated files
33 | run: ls -l *.rpm
34 | - name: Upload package to repository
35 | env:
36 | FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
37 | run: for f in *.rpm; do curl -F package=@$f https://$FURY_TOKEN@push.fury.io/miniflux/; done
38 |
--------------------------------------------------------------------------------
/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 | sess := session.New(h.store, request.SessionID(r))
18 | view := view.New(h.tpl, r, sess)
19 |
20 | user, err := h.store.UserByID(request.UserID(r))
21 | if err != nil {
22 | html.ServerError(w, r, err)
23 | return
24 | }
25 |
26 | view.Set("form", &form.APIKeyForm{})
27 | view.Set("menu", "settings")
28 | view.Set("user", user)
29 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
30 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
31 |
32 | html.OK(w, r, view.Render("create_api_key"))
33 | }
34 |
--------------------------------------------------------------------------------
/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/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 | ShowPrev bool
13 | NextOffset int
14 | PrevOffset int
15 | SearchQuery string
16 | }
17 |
18 | func getPagination(route string, total, offset, nbItemsPerPage int) pagination {
19 | nextOffset := 0
20 | prevOffset := 0
21 | showNext := (total - offset) > nbItemsPerPage
22 | showPrev := offset > 0
23 |
24 | if showNext {
25 | nextOffset = offset + nbItemsPerPage
26 | }
27 |
28 | if showPrev {
29 | prevOffset = offset - nbItemsPerPage
30 | }
31 |
32 | return pagination{
33 | Route: route,
34 | Total: total,
35 | Offset: offset,
36 | ItemsPerPage: nbItemsPerPage,
37 | ShowNext: showNext,
38 | NextOffset: nextOffset,
39 | ShowPrev: showPrev,
40 | PrevOffset: prevOffset,
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packaging/docker/distroless/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:latest AS build
2 | ENV CGO_ENABLED=0
3 | ADD . /go/src/app
4 | WORKDIR /go/src/app
5 | RUN go build \
6 | -o miniflux \
7 | -ldflags="-s -w -X 'miniflux.app/v2/internal/version.Version=`git describe --tags --abbrev=0`' -X 'miniflux.app/v2/internal/version.Commit=`git rev-parse --short HEAD`' -X 'miniflux.app/v2/internal/version.BuildDate=`date +%FT%T%z`'" \
8 | main.go
9 |
10 | FROM gcr.io/distroless/base
11 |
12 | LABEL org.opencontainers.image.title=Miniflux
13 | LABEL org.opencontainers.image.description="Miniflux is a minimalist and opinionated feed reader"
14 | LABEL org.opencontainers.image.vendor="Frédéric Guillot"
15 | LABEL org.opencontainers.image.licenses=Apache-2.0
16 | LABEL org.opencontainers.image.url=https://miniflux.app
17 | LABEL org.opencontainers.image.source=https://github.com/miniflux/v2
18 | LABEL org.opencontainers.image.documentation=https://miniflux.app/docs/
19 |
20 | EXPOSE 8080
21 | ENV LISTEN_ADDR 0.0.0.0:8080
22 | COPY --from=build /go/src/app/miniflux /usr/bin/miniflux
23 | USER nonroot:nonroot
24 | CMD ["/usr/bin/miniflux"]
25 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/contrib/ansible/roles/mgrote.miniflux/tasks/main.yml:
--------------------------------------------------------------------------------
1 | - name: add Apt-key for miniflux-repo
2 | become: yes
3 | apt_key:
4 | url: https://apt.miniflux.app/KEY.gpg
5 | state: present
6 |
7 | - name: add miniflux-repo
8 | become: yes
9 | apt_repository:
10 | repo: 'deb https://apt.miniflux.app/ /'
11 | state: present
12 | filename: miniflux_repo
13 | update_cache: yes
14 |
15 | - name: install miniflux
16 | become: yes
17 | apt:
18 | name: miniflux
19 | state: present
20 |
21 | - name: add miniflux linux_user
22 | become: yes
23 | user:
24 | name: "{{ miniflux_linux_user }}"
25 | home: "/var/empty"
26 | create_home: "no"
27 | system: "yes"
28 | shell: "/bin/false"
29 |
30 | - name: create directory "/etc/miniflux.d"
31 | become: yes
32 | file:
33 | path: /etc/miniflux.d
34 | state: directory
35 |
36 | - name: copy miniflux.conf
37 | become: yes
38 | template:
39 | src: "miniflux.conf"
40 | dest: "/etc/miniflux.conf"
41 | notify:
42 | - start_miniflux.service
43 | - miniflux_wait
44 |
--------------------------------------------------------------------------------
/internal/tests/import_export_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | //go:build integration
5 | // +build integration
6 |
7 | package tests
8 |
9 | import (
10 | "bytes"
11 | "io"
12 | "strings"
13 | "testing"
14 | )
15 |
16 | func TestExport(t *testing.T) {
17 | client := createClient(t)
18 |
19 | output, err := client.Export()
20 | if err != nil {
21 | t.Fatal(err)
22 | }
23 |
24 | if !strings.HasPrefix(string(output), "
33 |
34 |
35 |
36 |
37 |
38 |
39 | `
40 |
41 | b := bytes.NewReader([]byte(data))
42 | err := client.Import(io.NopCloser(b))
43 | if err != nil {
44 | t.Fatal(err)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/internal/template/templates/views/create_category.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.new_category.title" }}{{ end }}
2 |
3 | {{ define "content"}}
4 |
12 |
13 |
27 | {{ end }}
28 |
--------------------------------------------------------------------------------
/internal/tests/version_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | //go:build integration
5 | // +build integration
6 |
7 | package tests
8 |
9 | import (
10 | "testing"
11 |
12 | miniflux "miniflux.app/v2/client"
13 | )
14 |
15 | func TestVersionEndpoint(t *testing.T) {
16 | client := miniflux.New(testBaseURL, testAdminUsername, testAdminPassword)
17 | version, err := client.Version()
18 | if err != nil {
19 | t.Fatal(err)
20 | }
21 |
22 | if version.Version == "" {
23 | t.Fatal(`Version should not be empty`)
24 | }
25 |
26 | if version.Commit == "" {
27 | t.Fatal(`Commit should not be empty`)
28 | }
29 |
30 | if version.BuildDate == "" {
31 | t.Fatal(`Build date should not be empty`)
32 | }
33 |
34 | if version.GoVersion == "" {
35 | t.Fatal(`Go version should not be empty`)
36 | }
37 |
38 | if version.Compiler == "" {
39 | t.Fatal(`Compiler should not be empty`)
40 | }
41 |
42 | if version.Arch == "" {
43 | t.Fatal(`Arch should not be empty`)
44 | }
45 |
46 | if version.OS == "" {
47 | t.Fatal(`OS should not be empty`)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/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 | sess := session.New(h.store, request.SessionID(r))
17 | view := view.New(h.tpl, r, sess)
18 |
19 | user, err := h.store.UserByID(request.UserID(r))
20 | if err != nil {
21 | html.ServerError(w, r, err)
22 | return
23 | }
24 |
25 | apiKeys, err := h.store.APIKeys(user.ID)
26 | if err != nil {
27 | html.ServerError(w, r, err)
28 | return
29 | }
30 |
31 | view.Set("apiKeys", apiKeys)
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 |
37 | html.OK(w, r, view.Render("api_keys"))
38 | }
39 |
--------------------------------------------------------------------------------
/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 | sess := session.New(h.store, request.SessionID(r))
18 | view := view.New(h.tpl, r, sess)
19 |
20 | user, err := h.store.UserByID(request.UserID(r))
21 | if err != nil {
22 | html.ServerError(w, r, err)
23 | return
24 | }
25 |
26 | if !user.IsAdmin {
27 | html.Forbidden(w, r)
28 | return
29 | }
30 |
31 | view.Set("form", &form.UserForm{})
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 |
37 | html.OK(w, r, view.Render("create_user"))
38 | }
39 |
--------------------------------------------------------------------------------
/packaging/docker/alpine/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:alpine AS build
2 | ENV CGO_ENABLED=0
3 | RUN apk add --no-cache --update git
4 | ADD . /go/src/app
5 | WORKDIR /go/src/app
6 | RUN go build \
7 | -o miniflux \
8 | -ldflags="-s -w -X 'miniflux.app/v2/internal/version.Version=`git describe --tags --abbrev=0`' -X 'miniflux.app/v2/internal/version.Commit=`git rev-parse --short HEAD`' -X 'miniflux.app/v2/internal/version.BuildDate=`date +%FT%T%z`'" \
9 | main.go
10 |
11 | FROM alpine:latest
12 |
13 | LABEL org.opencontainers.image.title=Miniflux
14 | LABEL org.opencontainers.image.description="Miniflux is a minimalist and opinionated feed reader"
15 | LABEL org.opencontainers.image.vendor="Frédéric Guillot"
16 | LABEL org.opencontainers.image.licenses=Apache-2.0
17 | LABEL org.opencontainers.image.url=https://miniflux.app
18 | LABEL org.opencontainers.image.source=https://github.com/miniflux/v2
19 | LABEL org.opencontainers.image.documentation=https://miniflux.app/docs/
20 |
21 | EXPOSE 8080
22 | ENV LISTEN_ADDR 0.0.0.0:8080
23 | RUN apk --no-cache add ca-certificates tzdata
24 | COPY --from=build /go/src/app/miniflux /usr/bin/miniflux
25 | USER nobody
26 | CMD ["/usr/bin/miniflux"]
27 |
--------------------------------------------------------------------------------
/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="%d", UserID="%d", IP="%s", Token="%s"`, 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/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_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_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/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 | // CategoryRequest represents the request to create or update a category.
23 | type CategoryRequest struct {
24 | Title string `json:"title"`
25 | HideGlobally string `json:"hide_globally"`
26 | }
27 |
28 | // Patch updates category fields.
29 | func (cr *CategoryRequest) Patch(category *Category) {
30 | category.Title = cr.Title
31 | category.HideGlobally = cr.HideGlobally != ""
32 | }
33 |
34 | // Categories represents a list of categories.
35 | type Categories []*Category
36 |
--------------------------------------------------------------------------------
/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 | import (
7 | "strings"
8 |
9 | "miniflux.app/v2/internal/reader/sanitizer"
10 | )
11 |
12 | // DublinCoreFeedElement represents Dublin Core feed XML elements.
13 | type DublinCoreFeedElement struct {
14 | DublinCoreCreator string `xml:"http://purl.org/dc/elements/1.1/ channel>creator"`
15 | }
16 |
17 | func (feed *DublinCoreFeedElement) GetSanitizedCreator() string {
18 | return strings.TrimSpace(sanitizer.StripTags(feed.DublinCoreCreator))
19 | }
20 |
21 | // DublinCoreItemElement represents Dublin Core entry XML elements.
22 | type DublinCoreItemElement struct {
23 | DublinCoreDate string `xml:"http://purl.org/dc/elements/1.1/ date"`
24 | DublinCoreCreator string `xml:"http://purl.org/dc/elements/1.1/ creator"`
25 | DublinCoreContent string `xml:"http://purl.org/rss/1.0/modules/content/ encoded"`
26 | }
27 |
28 | func (item *DublinCoreItemElement) GetSanitizedCreator() string {
29 | return strings.TrimSpace(sanitizer.StripTags(item.DublinCoreCreator))
30 | }
31 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/contrib/docker-compose/caddy.yml:
--------------------------------------------------------------------------------
1 | services:
2 | caddy:
3 | image: caddy:2
4 | container_name: caddy
5 | depends_on:
6 | - miniflux
7 | ports:
8 | - "80:80"
9 | - "443:443"
10 | volumes:
11 | - $PWD/Caddyfile:/etc/caddy/Caddyfile
12 | - caddy_data:/data
13 | - caddy_config:/config
14 | miniflux:
15 | image: ${MINIFLUX_IMAGE:-miniflux/miniflux:latest}
16 | container_name: miniflux
17 | depends_on:
18 | db:
19 | condition: service_healthy
20 | environment:
21 | - DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
22 | - RUN_MIGRATIONS=1
23 | - CREATE_ADMIN=1
24 | - ADMIN_USERNAME=admin
25 | - ADMIN_PASSWORD=test123
26 | - BASE_URL=https://miniflux.example.org
27 | db:
28 | image: postgres:15
29 | container_name: postgres
30 | environment:
31 | - POSTGRES_USER=miniflux
32 | - POSTGRES_PASSWORD=secret
33 | volumes:
34 | - miniflux-db:/var/lib/postgresql/data
35 | healthcheck:
36 | test: ["CMD", "pg_isready", "-U", "miniflux"]
37 | interval: 10s
38 | start_period: 30s
39 | volumes:
40 | miniflux-db:
41 | caddy_data:
42 | caddy_config:
43 |
--------------------------------------------------------------------------------
/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 readtime provides a function to estimate the reading time of an article.
5 | package readingtime
6 |
7 | import (
8 | "math"
9 | "strings"
10 | "unicode/utf8"
11 |
12 | "miniflux.app/v2/internal/reader/sanitizer"
13 |
14 | "github.com/abadojack/whatlanggo"
15 | )
16 |
17 | // EstimateReadingTime returns the estimated reading time of an article in minute.
18 | func EstimateReadingTime(content string, defaultReadingSpeed, cjkReadingSpeed int) int {
19 | sanitizedContent := sanitizer.StripTags(content)
20 | langInfo := whatlanggo.Detect(sanitizedContent)
21 |
22 | var timeToReadInt int
23 | if langInfo.IsReliable() && (langInfo.Lang == whatlanggo.Jpn || langInfo.Lang == whatlanggo.Cmn || langInfo.Lang == whatlanggo.Kor) {
24 | timeToReadInt = int(math.Ceil(float64(utf8.RuneCountInString(sanitizedContent)) / float64(cjkReadingSpeed)))
25 | } else {
26 | nbOfWords := len(strings.Fields(sanitizedContent))
27 | timeToReadInt = int(math.Ceil(float64(nbOfWords) / float64(defaultReadingSpeed)))
28 | }
29 |
30 | return timeToReadInt
31 | }
32 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | switch DetectFeedFormat(r) {
23 | case FormatAtom:
24 | r.Seek(0, io.SeekStart)
25 | return atom.Parse(baseURL, r)
26 | case FormatRSS:
27 | r.Seek(0, io.SeekStart)
28 | return rss.Parse(baseURL, r)
29 | case FormatJSON:
30 | r.Seek(0, io.SeekStart)
31 | return json.Parse(baseURL, r)
32 | case FormatRDF:
33 | r.Seek(0, io.SeekStart)
34 | return rdf.Parse(baseURL, r)
35 | default:
36 | return nil, ErrFeedFormatNotDetected
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/internal/locale/plural_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 TestPluralRules(t *testing.T) {
9 | scenarios := map[string]map[int]int{
10 | "default": {
11 | 1: 0,
12 | 2: 1,
13 | 5: 1,
14 | },
15 | "ar_AR": {
16 | 0: 0,
17 | 1: 1,
18 | 2: 2,
19 | 5: 3,
20 | 11: 4,
21 | 200: 5,
22 | },
23 | "cs_CZ": {
24 | 1: 0,
25 | 2: 1,
26 | 5: 2,
27 | },
28 | "pl_PL": {
29 | 1: 0,
30 | 2: 1,
31 | 5: 2,
32 | },
33 | "pt_BR": {
34 | 1: 0,
35 | 2: 1,
36 | 5: 1,
37 | },
38 | "ru_RU": {
39 | 1: 0,
40 | 2: 1,
41 | 5: 2,
42 | },
43 | "sr_RS": {
44 | 1: 0,
45 | 2: 1,
46 | 5: 2,
47 | },
48 | "zh_CN": {
49 | 1: 0,
50 | 5: 0,
51 | },
52 | }
53 |
54 | for rule, values := range scenarios {
55 | for input, expected := range values {
56 | result := pluralForms[rule](input)
57 | if result != expected {
58 | t.Errorf(`Unexpected result for %q rule, got %d instead of %d for %d as input`, rule, result, expected, input)
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/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.WithHeader("Content-Type", "image/png")
35 | case ".svg":
36 | b.WithHeader("Content-Type", "image/svg+xml")
37 | }
38 |
39 | b.WithoutCompression()
40 | b.WithBody(blob)
41 | b.Write()
42 | })
43 | }
44 |
--------------------------------------------------------------------------------
/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/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 | "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/http/route"
15 | "miniflux.app/v2/internal/ui/static"
16 | )
17 |
18 | func (h *handler) showJavascript(w http.ResponseWriter, r *http.Request) {
19 | filename := request.RouteStringParam(r, "name")
20 | etag, found := static.JavascriptBundleChecksums[filename]
21 | if !found {
22 | html.NotFound(w, r)
23 | return
24 | }
25 |
26 | response.New(w, r).WithCaching(etag, 48*time.Hour, func(b *response.Builder) {
27 | contents := static.JavascriptBundles[filename]
28 |
29 | if filename == "service-worker" {
30 | variables := fmt.Sprintf(`const OFFLINE_URL="%s";`, route.Path(h.router, "offline"))
31 | contents = append([]byte(variables)[:], contents[:]...)
32 | }
33 |
34 | b.WithHeader("Content-Type", "text/javascript; charset=utf-8")
35 | b.WithBody(contents)
36 | b.Write()
37 | })
38 | }
39 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | Miniflux API Client
2 | ===================
3 |
4 | [](https://pkg.go.dev/miniflux.app/v2/client)
5 |
6 | Client library for Miniflux REST API.
7 |
8 | Installation
9 | ------------
10 |
11 | ```bash
12 | go get -u miniflux.app/v2/client
13 | ```
14 |
15 | Example
16 | -------
17 |
18 | ```go
19 | package main
20 |
21 | import (
22 | "fmt"
23 | "os"
24 |
25 | miniflux "miniflux.app/v2/client"
26 | )
27 |
28 | func main() {
29 | // Authentication with username/password:
30 | client := miniflux.New("https://api.example.org", "admin", "secret")
31 |
32 | // Authentication with an API Key:
33 | client := miniflux.New("https://api.example.org", "my-secret-token")
34 |
35 | // Fetch all feeds.
36 | feeds, err := client.Feeds()
37 | if err != nil {
38 | fmt.Println(err)
39 | return
40 | }
41 | fmt.Println(feeds)
42 |
43 | // Backup your feeds to an OPML file.
44 | opml, err := client.Export()
45 | if err != nil {
46 | fmt.Println(err)
47 | return
48 | }
49 |
50 | err = os.WriteFile("opml.xml", opml, 0644)
51 | if err != nil {
52 | fmt.Println(err)
53 | return
54 | }
55 | }
56 | ```
57 |
--------------------------------------------------------------------------------
/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/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 | sess := session.New(h.store, request.SessionID(r))
17 | view := view.New(h.tpl, r, sess)
18 |
19 | user, err := h.store.UserByID(request.UserID(r))
20 | if err != nil {
21 | html.ServerError(w, r, err)
22 | return
23 | }
24 |
25 | if !user.IsAdmin {
26 | html.Forbidden(w, r)
27 | return
28 | }
29 |
30 | users, err := h.store.Users()
31 | if err != nil {
32 | html.ServerError(w, r, err)
33 | return
34 | }
35 |
36 | users.UseTimezone(user.Timezone)
37 |
38 | view.Set("users", users)
39 | view.Set("menu", "settings")
40 | view.Set("user", user)
41 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
42 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
43 |
44 | html.OK(w, r, view.Render("users"))
45 | }
46 |
--------------------------------------------------------------------------------
/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 {
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.NewDecoder(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 | return FormatAtom
47 | case "RDF":
48 | return FormatRDF
49 | }
50 | }
51 | }
52 |
53 | return FormatUnknown
54 | }
55 |
--------------------------------------------------------------------------------
/internal/validator/validator.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 | "fmt"
8 | "net/url"
9 | "regexp"
10 | )
11 |
12 | // ValidateRange makes sure the offset/limit values are valid.
13 | func ValidateRange(offset, limit int) error {
14 | if offset < 0 {
15 | return fmt.Errorf(`Offset value should be >= 0`)
16 | }
17 |
18 | if limit < 0 {
19 | return fmt.Errorf(`Limit value should be >= 0`)
20 | }
21 |
22 | return nil
23 | }
24 |
25 | // ValidateDirection makes sure the sorting direction is valid.
26 | func ValidateDirection(direction string) error {
27 | switch direction {
28 | case "asc", "desc":
29 | return nil
30 | }
31 |
32 | return fmt.Errorf(`Invalid direction, valid direction values are: "asc" or "desc"`)
33 | }
34 |
35 | // IsValidRegex verifies if the regex can be compiled.
36 | func IsValidRegex(expr string) bool {
37 | _, err := regexp.Compile(expr)
38 | return err == nil
39 | }
40 |
41 | // IsValidURL verifies if the provided value is a valid absolute URL.
42 | func IsValidURL(absoluteURL string) bool {
43 | _, err := url.ParseRequestURI(absoluteURL)
44 | return err == nil
45 | }
46 |
--------------------------------------------------------------------------------
/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/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 | sess := session.New(h.store, request.SessionID(r))
17 | view := view.New(h.tpl, r, sess)
18 |
19 | user, err := h.store.UserByID(request.UserID(r))
20 | if err != nil {
21 | html.ServerError(w, r, err)
22 | return
23 | }
24 |
25 | sessions, err := h.store.UserSessions(user.ID)
26 | if err != nil {
27 | html.ServerError(w, r, err)
28 | return
29 | }
30 |
31 | sessions.UseTimezone(user.Timezone)
32 |
33 | view.Set("currentSessionToken", request.UserSessionToken(r))
34 | view.Set("sessions", sessions)
35 | view.Set("menu", "settings")
36 | view.Set("user", user)
37 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
38 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
39 |
40 | html.OK(w, r, view.Render("sessions"))
41 | }
42 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "gomod"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | reviewers:
8 | - "fguillot"
9 | assignees:
10 | - "fguillot"
11 |
12 | - package-ecosystem: "docker"
13 | directory: "/packaging/docker/alpine"
14 | schedule:
15 | interval: "weekly"
16 | reviewers:
17 | - "fguillot"
18 | assignees:
19 | - "fguillot"
20 |
21 | - package-ecosystem: "docker"
22 | directory: "/packaging/docker/distroless"
23 | schedule:
24 | interval: "weekly"
25 | reviewers:
26 | - "fguillot"
27 | assignees:
28 | - "fguillot"
29 |
30 | - package-ecosystem: "docker"
31 | directory: "packaging/debian"
32 | schedule:
33 | interval: "weekly"
34 | reviewers:
35 | - "fguillot"
36 | assignees:
37 | - "fguillot"
38 |
39 | - package-ecosystem: "docker"
40 | directory: "packaging/rpm"
41 | schedule:
42 | interval: "weekly"
43 | reviewers:
44 | - "fguillot"
45 | assignees:
46 | - "fguillot"
47 |
48 | - package-ecosystem: "github-actions"
49 | directory: "/"
50 | schedule:
51 | interval: "weekly"
52 | reviewers:
53 | - "fguillot"
54 | assignees:
55 | - "fguillot"
56 |
--------------------------------------------------------------------------------
/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 | // In this case, the provided date is already converted to the user timezone by Postgres,
16 | // but the timezone information is not set in the time struct.
17 | // We cannot use time.In() because the date will be converted a second time.
18 | t = time.Date(
19 | t.Year(),
20 | t.Month(),
21 | t.Day(),
22 | t.Hour(),
23 | t.Minute(),
24 | t.Second(),
25 | t.Nanosecond(),
26 | userTimezone,
27 | )
28 | } else if t.Location() != userTimezone {
29 | t = t.In(userTimezone)
30 | }
31 |
32 | return t
33 | }
34 |
35 | // Now returns the current time with the given timezone.
36 | func Now(tz string) time.Time {
37 | return time.Now().In(getLocation(tz))
38 | }
39 |
40 | func getLocation(tz string) *time.Location {
41 | loc, err := time.LoadLocation(tz)
42 | if err != nil {
43 | loc = time.Local
44 | }
45 | return loc
46 | }
47 |
--------------------------------------------------------------------------------
/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/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.CategoryRequest) *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.CategoryRequest) *locale.LocalizedError {
27 | if request.Title == "" {
28 | return locale.NewLocalizedError("error.title_required")
29 | }
30 |
31 | if store.AnotherCategoryExists(userID, categoryID, request.Title) {
32 | return locale.NewLocalizedError("error.category_already_exists")
33 | }
34 |
35 | return nil
36 | }
37 |
--------------------------------------------------------------------------------
/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 createAdmin(store *storage.Storage) {
16 | userCreationRequest := &model.UserCreationRequest{
17 | Username: config.Opts.AdminUsername(),
18 | Password: config.Opts.AdminPassword(),
19 | IsAdmin: true,
20 | }
21 |
22 | if userCreationRequest.Username == "" || userCreationRequest.Password == "" {
23 | userCreationRequest.Username, userCreationRequest.Password = askCredentials()
24 | }
25 |
26 | if store.UserExists(userCreationRequest.Username) {
27 | slog.Info("Skipping admin user creation because it already exists",
28 | slog.String("username", userCreationRequest.Username),
29 | )
30 | return
31 | }
32 |
33 | if validationErr := validator.ValidateUserCreationWithPassword(store, userCreationRequest); validationErr != nil {
34 | printErrorAndExit(validationErr.Error())
35 | }
36 |
37 | if _, err := store.CreateUser(userCreationRequest); err != nil {
38 | printErrorAndExit(err)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/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 | return m
43 | }
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/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/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.SessionData.Challenge, s.SessionData.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/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 |
11 | // Cookie names.
12 | const (
13 | CookieAppSessionID = "MinifluxAppSessionID"
14 | CookieUserSessionID = "MinifluxUserSessionID"
15 |
16 | // Cookie duration in days.
17 | cookieDuration = 30
18 | )
19 |
20 | // New creates a new cookie.
21 | func New(name, value string, isHTTPS bool, path string) *http.Cookie {
22 | return &http.Cookie{
23 | Name: name,
24 | Value: value,
25 | Path: basePath(path),
26 | Secure: isHTTPS,
27 | HttpOnly: true,
28 | Expires: time.Now().Add(cookieDuration * 24 * time.Hour),
29 | SameSite: http.SameSiteLaxMode,
30 | }
31 | }
32 |
33 | // Expired returns an expired cookie.
34 | func Expired(name string, isHTTPS bool, path string) *http.Cookie {
35 | return &http.Cookie{
36 | Name: name,
37 | Value: "",
38 | Path: basePath(path),
39 | Secure: isHTTPS,
40 | HttpOnly: true,
41 | MaxAge: -1,
42 | Expires: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
43 | SameSite: http.SameSiteLaxMode,
44 | }
45 | }
46 |
47 | func basePath(path string) string {
48 | if path == "" {
49 | return "/"
50 | }
51 | return path
52 | }
53 |
--------------------------------------------------------------------------------
/internal/template/templates/views/import.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.import.title" }}{{ end }}
2 |
3 | {{ define "content"}}
4 |
8 |
9 | {{ if .errorMessage }}
10 | {{ .errorMessage }}
11 | {{ end }}
12 |
13 |
23 |
24 |
34 |
35 | {{ end }}
36 |
--------------------------------------------------------------------------------
/internal/model/enclosure.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 | import "strings"
6 |
7 | // Enclosure represents an attachment.
8 | type Enclosure struct {
9 | ID int64 `json:"id"`
10 | UserID int64 `json:"user_id"`
11 | EntryID int64 `json:"entry_id"`
12 | URL string `json:"url"`
13 | MimeType string `json:"mime_type"`
14 | Size int64 `json:"size"`
15 | MediaProgression int64 `json:"media_progression"`
16 | }
17 |
18 | // Html5MimeType will modify the actual MimeType to allow direct playback from HTML5 player for some kind of MimeType
19 | func (e Enclosure) Html5MimeType() string {
20 | if strings.HasPrefix(e.MimeType, "video") {
21 | switch e.MimeType {
22 | // Solution from this stackoverflow discussion:
23 | // https://stackoverflow.com/questions/15277147/m4v-mimetype-video-mp4-or-video-m4v/66945470#66945470
24 | // tested at the time of this commit (06/2023) on latest Firefox & Vivaldi on this feed
25 | // https://www.florenceporcel.com/podcast/lfhdu.xml
26 | case "video/m4v":
27 | return "video/x-m4v"
28 | }
29 | }
30 | return e.MimeType
31 | }
32 |
33 | // EnclosureList represents a list of attachments.
34 | type EnclosureList []*Enclosure
35 |
--------------------------------------------------------------------------------
/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 | sess := session.New(h.store, request.SessionID(r))
26 | view := view.New(h.tpl, r, sess)
27 | view.Set("version", version.Version)
28 | view.Set("commit", version.Commit)
29 | view.Set("build_date", version.BuildDate)
30 | view.Set("menu", "settings")
31 | view.Set("user", user)
32 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
33 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
34 | view.Set("globalConfigOptions", config.Opts.SortedOptions(true))
35 | view.Set("postgres_version", h.store.DatabaseVersion())
36 | view.Set("go_version", runtime.Version())
37 |
38 | html.OK(w, r, view.Render("about"))
39 | }
40 |
--------------------------------------------------------------------------------
/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/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 | "io"
10 |
11 | "golang.org/x/oauth2"
12 |
13 | "miniflux.app/v2/internal/crypto"
14 | )
15 |
16 | type Authorization struct {
17 | url string
18 | state string
19 | codeVerifier string
20 | }
21 |
22 | func (u *Authorization) RedirectURL() string {
23 | return u.url
24 | }
25 |
26 | func (u *Authorization) State() string {
27 | return u.state
28 | }
29 |
30 | func (u *Authorization) CodeVerifier() string {
31 | return u.codeVerifier
32 | }
33 |
34 | func GenerateAuthorization(config *oauth2.Config) *Authorization {
35 | codeVerifier := crypto.GenerateRandomStringHex(32)
36 |
37 | sha2 := sha256.New()
38 | io.WriteString(sha2, codeVerifier)
39 | codeChallenge := base64.RawURLEncoding.EncodeToString(sha2.Sum(nil))
40 |
41 | state := crypto.GenerateRandomStringHex(24)
42 |
43 | authUrl := config.AuthCodeURL(
44 | state,
45 | oauth2.SetAuthURLParam("code_challenge_method", "S256"),
46 | oauth2.SetAuthURLParam("code_challenge", codeChallenge),
47 | )
48 |
49 | return &Authorization{
50 | url: authUrl,
51 | state: state,
52 | codeVerifier: codeVerifier,
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/internal/reader/encoding/encoding.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package encoding // import "miniflux.app/v2/internal/reader/encoding"
5 |
6 | import (
7 | "bytes"
8 | "io"
9 | "unicode/utf8"
10 |
11 | "golang.org/x/net/html/charset"
12 | )
13 |
14 | // CharsetReader is used when the XML encoding is specified for the input document.
15 | //
16 | // The document is converted in UTF-8 only if a different encoding is specified
17 | // and the document is not already UTF-8.
18 | //
19 | // Several edge cases could exists:
20 | //
21 | // - Feeds with encoding specified only in Content-Type header and not in XML document
22 | // - Feeds with encoding specified in both places
23 | // - Feeds with encoding specified only in XML document and not in HTTP header
24 | // - Feeds with wrong encoding defined and already in UTF-8
25 | func CharsetReader(label string, input io.Reader) (io.Reader, error) {
26 | buffer, _ := io.ReadAll(input)
27 | r := bytes.NewReader(buffer)
28 |
29 | // The document is already UTF-8, do not do anything (avoid double-encoding).
30 | // That means the specified encoding in XML prolog is wrong.
31 | if utf8.Valid(buffer) {
32 | return r, nil
33 | }
34 |
35 | // Transform document to UTF-8 from the specified encoding in XML prolog.
36 | return charset.NewReaderLabel(label, r)
37 | }
38 |
--------------------------------------------------------------------------------
/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 | json2 "encoding/json"
8 | "io"
9 | "net/http"
10 |
11 | "miniflux.app/v2/internal/http/request"
12 | "miniflux.app/v2/internal/http/response/json"
13 | )
14 |
15 | func (h *handler) saveEnclosureProgression(w http.ResponseWriter, r *http.Request) {
16 | enclosureID := request.RouteInt64Param(r, "enclosureID")
17 | enclosure, err := h.store.GetEnclosure(enclosureID)
18 | if err != nil {
19 | json.ServerError(w, r, err)
20 | return
21 | }
22 |
23 | if enclosure == nil {
24 | json.NotFound(w, r)
25 | return
26 | }
27 |
28 | type enclosurePositionSaveRequest struct {
29 | Progression int64 `json:"progression"`
30 | }
31 |
32 | var postData enclosurePositionSaveRequest
33 | body, err := io.ReadAll(r.Body)
34 | if err != nil {
35 | json.ServerError(w, r, err)
36 | return
37 | }
38 |
39 | json2.Unmarshal(body, &postData)
40 | if err != nil {
41 | json.ServerError(w, r, err)
42 | return
43 | }
44 | enclosure.MediaProgression = postData.Progression
45 |
46 | err = h.store.UpdateEnclosure(enclosure)
47 | if err != nil {
48 | json.ServerError(w, r, err)
49 | return
50 | }
51 |
52 | json.Created(w, r, map[string]string{"message": "saved"})
53 | }
54 |
--------------------------------------------------------------------------------
/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.HasIcon(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/integration/rssbridge/rssbridge.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package rssbridge // import "miniflux.app/integration/rssbridge"
5 |
6 | import (
7 | "encoding/json"
8 | "fmt"
9 | "net/http"
10 | "net/url"
11 | )
12 |
13 | type Bridge struct {
14 | URL string `json:"url"`
15 | BridgeMeta BridgeMeta `json:"bridgeMeta"`
16 | }
17 |
18 | type BridgeMeta struct {
19 | Name string `json:"name"`
20 | }
21 |
22 | func DetectBridges(rssbridgeURL, websiteURL string) (bridgeResponse []Bridge, err error) {
23 | u, err := url.Parse(rssbridgeURL)
24 | if err != nil {
25 | return nil, err
26 | }
27 | values := u.Query()
28 | values.Add("action", "findfeed")
29 | values.Add("format", "atom")
30 | values.Add("url", websiteURL)
31 | u.RawQuery = values.Encode()
32 |
33 | response, err := http.Get(u.String())
34 | if err != nil {
35 | return nil, fmt.Errorf("RSS-Bridge: unable to excute request: %w", err)
36 | }
37 | defer response.Body.Close()
38 | if response.StatusCode == http.StatusNotFound {
39 | return
40 | }
41 | if response.StatusCode > 400 {
42 | return nil, fmt.Errorf("RSS-Bridge: unexpected status code %d", response.StatusCode)
43 | }
44 | if err := json.NewDecoder(response.Body).Decode(&bridgeResponse); err != nil {
45 | return nil, fmt.Errorf("RSS-Bridge: unable to decode bridge response: %w", err)
46 | }
47 | return
48 | }
49 |
--------------------------------------------------------------------------------
/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 | sess := session.New(h.store, request.SessionID(r))
19 | view := view.New(h.tpl, r, sess)
20 |
21 | user, err := h.store.UserByID(request.UserID(r))
22 | if err != nil {
23 | html.ServerError(w, r, err)
24 | return
25 | }
26 |
27 | categories, err := h.store.Categories(user.ID)
28 | if err != nil {
29 | html.ServerError(w, r, err)
30 | return
31 | }
32 |
33 | view.Set("categories", categories)
34 | view.Set("menu", "feeds")
35 | view.Set("user", user)
36 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
37 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
38 | view.Set("defaultUserAgent", config.Opts.HTTPClientUserAgent())
39 | view.Set("form", &form.SubscriptionForm{CategoryID: 0})
40 | view.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyConfigured())
41 |
42 | html.OK(w, r, view.Render("add_subscription"))
43 | }
44 |
--------------------------------------------------------------------------------
/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.
34 | func FindRemoteIP(r *http.Request) string {
35 | remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
36 | if err != nil {
37 | remoteIP = r.RemoteAddr
38 | }
39 | remoteIP = dropIPv6zone(remoteIP)
40 |
41 | // When listening on a Unix socket, RemoteAddr is empty.
42 | if remoteIP == "" {
43 | remoteIP = "127.0.0.1"
44 | }
45 |
46 | return remoteIP
47 | }
48 |
49 | func dropIPv6zone(address string) string {
50 | i := strings.IndexByte(address, '%')
51 | if i != -1 {
52 | address = address[:i]
53 | }
54 | return address
55 | }
56 |
--------------------------------------------------------------------------------
/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 | CategoryName: category,
38 | })
39 | } else if outline.Outlines.HasChildren() {
40 | subscriptions = append(subscriptions, getSubscriptionsFromOutlines(outline.Outlines, outline.Text)...)
41 | }
42 | }
43 | return subscriptions
44 | }
45 |
--------------------------------------------------------------------------------
/internal/template/templates/views/about.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.about.title" }}{{ end }}
2 |
3 | {{ define "content"}}
4 |
8 |
9 |
10 |
Miniflux
11 |
12 | - {{ t "page.about.version" }} {{ .version }}
13 | - Git Commit {{ .commit }}
14 | - {{ t "page.about.build_date" }} {{ .build_date }}
15 | {{ if .user.IsAdmin }}- {{ t "page.about.postgres_version" }} {{ .postgres_version }}
{{ end }}
16 | - {{t "page.about.go_version" }} {{ .go_version }}
17 |
18 |
19 |
20 |
21 |
{{ t "page.about.credits" }}
22 |
23 | - {{ t "page.about.author" }} Frédéric Guillot
24 | - {{ t "page.about.license" }} Apache 2.0
25 |
26 |
27 |
28 | {{ if .user.IsAdmin }}
29 |
30 |
{{ t "page.about.global_config_options" }}
31 |
32 | {{ range .globalConfigOptions }}
33 | {{ .Key }}={{ .Value }}
34 | {{ end }}
35 |
36 |
37 | {{ end }}
38 |
39 | {{ end }}
40 |
--------------------------------------------------------------------------------
/internal/ui/shared_entries.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) sharedEntries(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 | builder := h.store.NewEntryQueryBuilder(user.ID)
23 | builder.WithShareCodeNotEmpty()
24 | builder.WithSorting(user.EntryOrder, user.EntryDirection)
25 |
26 | entries, err := builder.GetEntries()
27 | if err != nil {
28 | html.ServerError(w, r, err)
29 | return
30 | }
31 |
32 | count, err := builder.CountEntries()
33 | if err != nil {
34 | html.ServerError(w, r, err)
35 | return
36 | }
37 |
38 | sess := session.New(h.store, request.SessionID(r))
39 | view := view.New(h.tpl, r, sess)
40 | view.Set("entries", entries)
41 | view.Set("total", count)
42 | view.Set("menu", "history")
43 | view.Set("user", user)
44 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
45 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
46 | view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
47 |
48 | html.OK(w, r, view.Render("shared_entries"))
49 | }
50 |
--------------------------------------------------------------------------------
/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 | // PushEntry 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/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 |
--------------------------------------------------------------------------------
/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 | make miniflux && \
13 | mkdir -p /build/debian && \
14 | cd /build && \
15 | cp /src/miniflux /build/ && \
16 | cp /src/miniflux.1 /build/ && \
17 | cp /src/LICENSE /build/ && \
18 | cp /src/packaging/miniflux.conf /build/ && \
19 | cp /src/packaging/systemd/miniflux.service /build/debian/ && \
20 | cp /src/packaging/debian/compat /build/debian/compat && \
21 | cp /src/packaging/debian/copyright /build/debian/copyright && \
22 | cp /src/packaging/debian/miniflux.manpages /build/debian/miniflux.manpages && \
23 | cp /src/packaging/debian/miniflux.postinst /build/debian/miniflux.postinst && \
24 | cp /src/packaging/debian/rules /build/debian/rules && \
25 | cp /src/packaging/debian/miniflux.dirs /build/debian/miniflux.dirs && \
26 | echo "miniflux ($PKG_VERSION) experimental; urgency=low" > /build/debian/changelog && \
27 | echo " * Miniflux version $PKG_VERSION" >> /build/debian/changelog && \
28 | echo " -- Frédéric Guillot $PKG_DATE" >> /build/debian/changelog && \
29 | sed "s/__PKG_ARCH__/${PKG_ARCH}/g" /src/packaging/debian/control > /build/debian/control && \
30 | dpkg-buildpackage -us -uc -b && \
31 | lintian --check --color always ../*.deb && \
32 | cp ../*.deb /pkg/
33 |
--------------------------------------------------------------------------------
/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 | "bytes"
8 | "encoding/xml"
9 | "fmt"
10 | "io"
11 |
12 | "miniflux.app/v2/internal/model"
13 | xml_decoder "miniflux.app/v2/internal/reader/xml"
14 | )
15 |
16 | type atomFeed interface {
17 | Transform(baseURL string) *model.Feed
18 | }
19 |
20 | // Parse returns a normalized feed struct from a Atom feed.
21 | func Parse(baseURL string, r io.Reader) (*model.Feed, error) {
22 | var buf bytes.Buffer
23 | tee := io.TeeReader(r, &buf)
24 |
25 | var rawFeed atomFeed
26 | if getAtomFeedVersion(tee) == "0.3" {
27 | rawFeed = new(atom03Feed)
28 | } else {
29 | rawFeed = new(atom10Feed)
30 | }
31 |
32 | if err := xml_decoder.NewDecoder(&buf).Decode(rawFeed); err != nil {
33 | return nil, fmt.Errorf("atom: unable to parse feed: %w", err)
34 | }
35 |
36 | return rawFeed.Transform(baseURL), nil
37 | }
38 |
39 | func getAtomFeedVersion(data io.Reader) string {
40 | decoder := xml_decoder.NewDecoder(data)
41 | for {
42 | token, _ := decoder.Token()
43 | if token == nil {
44 | break
45 | }
46 |
47 | if element, ok := token.(xml.StartElement); ok {
48 | if element.Name.Local == "feed" {
49 | for _, attr := range element.Attr {
50 | if attr.Name.Local == "version" && attr.Value == "0.3" {
51 | return "0.3"
52 | }
53 | }
54 | return "1.0"
55 | }
56 | }
57 | }
58 | return "1.0"
59 | }
60 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | permissions: read-all
3 |
4 | on:
5 | pull_request:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | unit-tests:
11 | name: Unit Tests
12 | runs-on: ${{ matrix.os }}
13 | strategy:
14 | max-parallel: 4
15 | matrix:
16 | os: [ubuntu-latest, windows-latest, macOS-latest]
17 | go-version: ["1.21"]
18 | steps:
19 | - name: Set up Go
20 | uses: actions/setup-go@v4
21 | with:
22 | go-version: ${{ matrix.go-version }}
23 | - name: Checkout
24 | uses: actions/checkout@v4
25 | - name: Run unit tests
26 | run: make test
27 |
28 | integration-tests:
29 | name: Integration Tests
30 | runs-on: ubuntu-latest
31 | services:
32 | postgres:
33 | image: postgres:9.5
34 | env:
35 | POSTGRES_USER: postgres
36 | POSTGRES_PASSWORD: postgres
37 | POSTGRES_DB: postgres
38 | ports:
39 | - 5432:5432
40 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
41 | steps:
42 | - name: Set up Go
43 | uses: actions/setup-go@v4
44 | with:
45 | go-version: "1.21"
46 | - name: Checkout
47 | uses: actions/checkout@v4
48 | - name: Install Postgres client
49 | run: sudo apt update && sudo apt install -y postgresql-client
50 | - name: Run integration tests
51 | run: make integration-test
52 | env:
53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54 | PGHOST: 127.0.0.1
55 | PGPASSWORD: postgres
56 |
--------------------------------------------------------------------------------
/internal/locale/catalog.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 (
7 | "embed"
8 | "encoding/json"
9 | "fmt"
10 | )
11 |
12 | type translationDict map[string]interface{}
13 | type catalog map[string]translationDict
14 |
15 | var defaultCatalog catalog
16 |
17 | //go:embed translations/*.json
18 | var translationFiles embed.FS
19 |
20 | // LoadCatalogMessages loads and parses all translations encoded in JSON.
21 | func LoadCatalogMessages() error {
22 | var err error
23 | defaultCatalog = make(catalog)
24 |
25 | for language := range AvailableLanguages() {
26 | defaultCatalog[language], err = loadTranslationFile(language)
27 | if err != nil {
28 | return err
29 | }
30 | }
31 | return nil
32 | }
33 |
34 | func loadTranslationFile(language string) (translationDict, error) {
35 | translationFileData, err := translationFiles.ReadFile(fmt.Sprintf("translations/%s.json", language))
36 | if err != nil {
37 | return nil, err
38 | }
39 |
40 | translationMessages, err := parseTranslationMessages(translationFileData)
41 | if err != nil {
42 | return nil, err
43 | }
44 |
45 | return translationMessages, nil
46 | }
47 |
48 | func parseTranslationMessages(data []byte) (translationDict, error) {
49 | var translationMessages translationDict
50 | if err := json.Unmarshal(data, &translationMessages); err != nil {
51 | return nil, fmt.Errorf(`invalid translation file: %w`, err)
52 | }
53 | return translationMessages, nil
54 | }
55 |
--------------------------------------------------------------------------------
/internal/template/templates/views/bookmark_entries.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.starred.title" }} ({{ .total }}){{ end }}
2 |
3 | {{ define "content"}}
4 |
7 |
8 | {{ if not .entries }}
9 | {{ t "alert.no_bookmark" }}
10 | {{ else }}
11 |
14 |
15 | {{ range .entries }}
16 |
17 |
26 | {{ template "item_meta" dict "user" $.user "entry" . "hasSaveEntry" $.hasSaveEntry }}
27 |
28 | {{ end }}
29 |
30 |
33 | {{ end }}
34 |
35 | {{ end }}
36 |
--------------------------------------------------------------------------------
/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 | sess := session.New(h.store, request.SessionID(r))
18 | view := view.New(h.tpl, r, sess)
19 |
20 | user, err := h.store.UserByID(request.UserID(r))
21 | if err != nil {
22 | html.ServerError(w, r, err)
23 | return
24 | }
25 |
26 | categoryID := request.RouteInt64Param(r, "categoryID")
27 | category, err := h.store.Category(request.UserID(r), categoryID)
28 | if err != nil {
29 | html.ServerError(w, r, err)
30 | return
31 | }
32 |
33 | if category == nil {
34 | html.NotFound(w, r)
35 | return
36 | }
37 |
38 | categoryForm := form.CategoryForm{
39 | Title: category.Title,
40 | HideGlobally: "",
41 | }
42 | if category.HideGlobally {
43 | categoryForm.HideGlobally = "checked"
44 | }
45 |
46 | view.Set("form", categoryForm)
47 | view.Set("category", category)
48 | view.Set("menu", "categories")
49 | view.Set("user", user)
50 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
51 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
52 |
53 | html.OK(w, r, view.Render("edit_category"))
54 | }
55 |
--------------------------------------------------------------------------------
/internal/template/templates/views/edit_category.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.edit_category.title" .category.Title }}{{ end }}
2 |
3 | {{ define "content"}}
4 |
18 |
19 |
38 | {{ end }}
39 |
--------------------------------------------------------------------------------
/internal/template/templates/views/search_entries.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.search.title" }} ({{ .total }}){{ end }}
2 |
3 | {{ define "content"}}
4 |
7 |
8 | {{ if not .entries }}
9 | {{ t "alert.no_search_result" }}
10 | {{ else }}
11 |
14 |
15 | {{ range .entries }}
16 |
17 |
26 | {{ template "item_meta" dict "user" $.user "entry" . "hasSaveEntry" $.hasSaveEntry }}
27 |
28 | {{ end }}
29 |
30 |
33 | {{ end }}
34 |
35 | {{ end }}
36 |
--------------------------------------------------------------------------------
/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 | sess := session.New(h.store, request.SessionID(r))
19 | view := view.New(h.tpl, r, sess)
20 |
21 | user, err := h.store.UserByID(request.UserID(r))
22 | if err != nil {
23 | html.ServerError(w, r, err)
24 | return
25 | }
26 |
27 | if !user.IsAdmin {
28 | html.Forbidden(w, r)
29 | return
30 | }
31 |
32 | userID := request.RouteInt64Param(r, "userID")
33 | selectedUser, err := h.store.UserByID(userID)
34 | if err != nil {
35 | html.ServerError(w, r, err)
36 | return
37 | }
38 |
39 | if selectedUser == nil {
40 | html.NotFound(w, r)
41 | return
42 | }
43 |
44 | userForm := &form.UserForm{
45 | Username: selectedUser.Username,
46 | IsAdmin: selectedUser.IsAdmin,
47 | }
48 |
49 | view.Set("form", userForm)
50 | view.Set("selected_user", selectedUser)
51 | view.Set("menu", "settings")
52 | view.Set("user", user)
53 | view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
54 | view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
55 |
56 | html.OK(w, r, view.Render("edit_user"))
57 | }
58 |
--------------------------------------------------------------------------------
/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 | if localizedError != nil {
49 | slog.Warn("Unable to refresh a feed",
50 | slog.Int64("user_id", job.UserID),
51 | slog.Int64("feed_id", job.FeedID),
52 | slog.Any("error", localizedError.Error()),
53 | )
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/internal/tests/subscription_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | //go:build integration
5 | // +build integration
6 |
7 | package tests
8 |
9 | import (
10 | "testing"
11 |
12 | miniflux "miniflux.app/v2/client"
13 | )
14 |
15 | func TestDiscoverSubscriptions(t *testing.T) {
16 | client := createClient(t)
17 | subscriptions, err := client.Discover(testWebsiteURL)
18 | if err != nil {
19 | t.Fatal(err)
20 | }
21 |
22 | if len(subscriptions) != 1 {
23 | t.Fatalf(`Invalid number of subscriptions, got "%v" instead of "%v"`, len(subscriptions), 2)
24 | }
25 |
26 | if subscriptions[0].Title != testSubscriptionTitle {
27 | t.Fatalf(`Invalid feed title, got "%v" instead of "%v"`, subscriptions[0].Title, testSubscriptionTitle)
28 | }
29 |
30 | if subscriptions[0].Type != "atom" {
31 | t.Fatalf(`Invalid feed type, got "%v" instead of "%v"`, subscriptions[0].Type, "atom")
32 | }
33 |
34 | if subscriptions[0].URL != testFeedURL {
35 | t.Fatalf(`Invalid feed URL, got "%v" instead of "%v"`, subscriptions[0].URL, testFeedURL)
36 | }
37 | }
38 |
39 | func TestDiscoverSubscriptionsWithInvalidURL(t *testing.T) {
40 | client := createClient(t)
41 | _, err := client.Discover("invalid")
42 | if err == nil {
43 | t.Fatal(`Invalid URLs should be rejected`)
44 | }
45 | }
46 |
47 | func TestDiscoverSubscriptionsWithNoSubscription(t *testing.T) {
48 | client := createClient(t)
49 | _, err := client.Discover(testBaseURL)
50 | if err != miniflux.ErrNotFound {
51 | t.Fatal(`A 404 should be returned when there is no subscription`)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/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/template/templates/views/create_user.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.new_user.title" }}{{ end }}
2 |
3 | {{ define "content"}}
4 |
8 |
9 |
31 | {{ end }}
32 |
--------------------------------------------------------------------------------
/internal/template/templates/views/sessions.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ t "page.sessions.title" }}{{ end }}
2 |
3 | {{ define "content"}}
4 |
8 |
9 |
10 |
11 | | {{ t "page.sessions.table.date" }} |
12 | {{ t "page.sessions.table.ip" }} |
13 | {{ t "page.sessions.table.user_agent" }} |
14 | {{ t "page.sessions.table.actions" }} |
15 |
16 | {{ range .sessions }}
17 |
18 | | {{ elapsed $.user.Timezone .CreatedAt }} |
19 | {{ .IP }} |
20 | {{ .UserAgent }} |
21 |
22 | {{ if eq .Token $.currentSessionToken }}
23 | {{ t "page.sessions.table.current_session" }}
24 | {{ else }}
25 | {{ icon "delete" }}{{ t "action.remove" }}
32 | {{ end }}
33 | |
34 |
35 | {{ end }}
36 |
37 |
38 | {{ end }}
39 |
--------------------------------------------------------------------------------
/internal/crypto/crypto.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package crypto // import "miniflux.app/v2/internal/crypto"
5 |
6 | import (
7 | "crypto/hmac"
8 | "crypto/rand"
9 | "crypto/sha256"
10 | "encoding/base64"
11 | "encoding/hex"
12 | "fmt"
13 |
14 | "golang.org/x/crypto/bcrypt"
15 | )
16 |
17 | // HashFromBytes returns a SHA-256 checksum of the input.
18 | func HashFromBytes(value []byte) string {
19 | sum := sha256.Sum256(value)
20 | return fmt.Sprintf("%x", sum)
21 | }
22 |
23 | // Hash returns a SHA-256 checksum of a string.
24 | func Hash(value string) string {
25 | return HashFromBytes([]byte(value))
26 | }
27 |
28 | // GenerateRandomBytes returns random bytes.
29 | func GenerateRandomBytes(size int) []byte {
30 | b := make([]byte, size)
31 | if _, err := rand.Read(b); err != nil {
32 | panic(err)
33 | }
34 |
35 | return b
36 | }
37 |
38 | // GenerateRandomString returns a random string.
39 | func GenerateRandomString(size int) string {
40 | return base64.URLEncoding.EncodeToString(GenerateRandomBytes(size))
41 | }
42 |
43 | // GenerateRandomStringHex returns a random hexadecimal string.
44 | func GenerateRandomStringHex(size int) string {
45 | return hex.EncodeToString(GenerateRandomBytes(size))
46 | }
47 |
48 | func HashPassword(password string) (string, error) {
49 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
50 | return string(bytes), err
51 | }
52 |
53 | func GenerateSHA256Hmac(secret string, data []byte) string {
54 | h := hmac.New(sha256.New, []byte(secret))
55 | h.Write(data)
56 | return hex.EncodeToString(h.Sum(nil))
57 | }
58 |
--------------------------------------------------------------------------------
/internal/template/templates/views/category_feeds.html:
--------------------------------------------------------------------------------
1 | {{ define "title"}}{{ .category.Title }} > {{ t "page.feeds.title" }} ({{ .total }}){{ end }}
2 |
3 | {{ define "content"}}
4 |
30 |
31 | {{ if not .feeds }}
32 | {{ t "alert.no_feed_in_category" }}
33 | {{ else }}
34 | {{ template "feed_list" dict "user" .user "feeds" .feeds "ParsingErrorCount" .ParsingErrorCount }}
35 | {{ end }}
36 |
37 | {{ end }}
38 |
--------------------------------------------------------------------------------