Skip to content

Commit

Permalink
Add "Share article" feature
Browse files Browse the repository at this point in the history
A new "shareCode" field is generated for each entry, and allows
unlogged users to access the entry through the /shared endpoint.
This feature is particularly useful to share articles from miniflux
to third-party users without having them to visit the original source.

The image proxy is disabled and special cache headers are proposed in
the shared page to avoid denial of service.
  • Loading branch information
Lesterpig authored and fguillot committed Mar 18, 2020
1 parent 1b86913 commit 41a2b7e
Show file tree
Hide file tree
Showing 24 changed files with 243 additions and 26 deletions.
1 change: 1 addition & 0 deletions client/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ type Entry struct {
Date time.Time `json:"published_at"`
Content string `json:"content"`
Author string `json:"author"`
ShareCode string `json:"share_code"`
Starred bool `json:"starred"`
Enclosures Enclosures `json:"enclosures,omitempty"`
Feed *Feed `json:"feed,omitempty"`
Expand Down
6 changes: 6 additions & 0 deletions crypto/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
)

Expand Down Expand Up @@ -36,3 +37,8 @@ func GenerateRandomBytes(size int) []byte {
func GenerateRandomString(size int) string {
return base64.URLEncoding.EncodeToString(GenerateRandomBytes(size))
}

// GenerateRandomStringHex returns a random hexadecimal string.
func GenerateRandomStringHex(size int) string {
return hex.EncodeToString(GenerateRandomBytes(size))
}
2 changes: 1 addition & 1 deletion database/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"miniflux.app/logger"
)

const schemaVersion = 27
const schemaVersion = 28

// Migrate executes database migrations.
func Migrate(db *sql.DB) {
Expand Down
4 changes: 4 additions & 0 deletions database/sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions database/sql/schema_version_28.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
alter table entries add column share_code text not null default '';
create unique index entries_share_code_idx on entries using btree(share_code) where share_code <> '';
40 changes: 30 additions & 10 deletions locale/translations.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions locale/translations/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
"entry.original.label": "Original-Artikel",
"entry.comments.label": "Kommentare",
"entry.comments.title": "Kommentare anzeigen",
"entry.share.label": "Teilen",
"entry.share.title": "Diesen Artikel teilen",
"page.unread.title": "Ungelesen",
"page.starred.title": "Lesezeichen",
"page.categories.title": "Kategorien",
Expand Down
2 changes: 2 additions & 0 deletions locale/translations/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
"entry.original.label": "Original",
"entry.comments.label": "Comments",
"entry.comments.title": "View Comments",
"entry.share.label": "Share",
"entry.share.title": "Share this article",
"page.unread.title": "Unread",
"page.starred.title": "Starred",
"page.categories.title": "Categories",
Expand Down
2 changes: 2 additions & 0 deletions locale/translations/es_ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
"entry.original.label": "Original",
"entry.comments.label": "Comentarios",
"entry.comments.title": "Ver comentarios",
"entry.share.label": "Comparta",
"entry.share.title": "Comparta este articulo",
"page.unread.title": "No leídos",
"page.starred.title": "Marcadores",
"page.categories.title": "Categorias",
Expand Down
2 changes: 2 additions & 0 deletions locale/translations/fr_FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
"entry.original.label": "Original",
"entry.comments.label": "Commentaires",
"entry.comments.title": "Voir les commentaires",
"entry.share.label": "Partager",
"entry.share.title": "Partager cet article",
"page.unread.title": "Non lus",
"page.starred.title": "Favoris",
"page.categories.title": "Catégories",
Expand Down
2 changes: 2 additions & 0 deletions locale/translations/it_IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
"entry.original.label": "Contenuto originale",
"entry.comments.label": "Commenti",
"entry.comments.title": "Mostra i commenti",
"entry.share.label": "Condividi",
"entry.share.title": "Condividi questo articolo",
"page.unread.title": "Da leggere",
"page.starred.title": "Preferiti",
"page.categories.title": "Categorie",
Expand Down
2 changes: 2 additions & 0 deletions locale/translations/ja_JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
"entry.original.label": "オリジナル",
"entry.comments.label": "コメント",
"entry.comments.title": "コメントを見る",
"entry.share.label": "共有",
"entry.share.title": "この記事を共有する",
"page.unread.title": "未読",
"page.starred.title": "星付き",
"page.categories.title": "カテゴリ",
Expand Down
2 changes: 2 additions & 0 deletions locale/translations/nl_NL.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
"entry.original.label": "Origineel",
"entry.comments.label": "Comments",
"entry.comments.title": "Bekijk de reacties",
"entry.share.label": "Deel",
"entry.share.title": "Deel dit artikel",
"page.unread.title": "Ongelezen",
"page.starred.title": "Favorieten",
"page.categories.title": "Categorieën",
Expand Down
2 changes: 2 additions & 0 deletions locale/translations/pl_PL.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
"entry.original.label": "Oryginalny artykuł",
"entry.comments.label": "Komentarze",
"entry.comments.title": "Zobacz komentarze",
"entry.share.label": "Podzielić się",
"entry.share.title": "Podzielić się ten artykuł",
"page.unread.title": "Nieprzeczytane",
"page.starred.title": "Oznaczone gwiazdką",
"page.categories.title": "Kategorie",
Expand Down
2 changes: 2 additions & 0 deletions locale/translations/ru_RU.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
"entry.original.label": "Оригинал",
"entry.comments.label": "Комментарии",
"entry.comments.title": "Показать комментарии",
"entry.share.label": "поделиться",
"entry.share.title": "поделиться эту статью",
"page.unread.title": "Непрочитанное",
"page.starred.title": "Избранное",
"page.categories.title": "Категории",
Expand Down
2 changes: 2 additions & 0 deletions locale/translations/zh_CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
"entry.original.label": "原始内容",
"entry.comments.label": "评论",
"entry.comments.title": "查看评论",
"entry.share.label": "分享",
"entry.share.title": "分享这篇文章",
"page.unread.title": "未读",
"page.starred.title": "星标",
"page.categories.title": "分类",
Expand Down
1 change: 1 addition & 0 deletions model/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type Entry struct {
Date time.Time `json:"published_at"`
Content string `json:"content"`
Author string `json:"author"`
ShareCode string `json:"share_code"`
Starred bool `json:"starred"`
Enclosures EnclosureList `json:"enclosures,omitempty"`
Feed *Feed `json:"feed,omitempty"`
Expand Down
33 changes: 33 additions & 0 deletions storage/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"time"

"miniflux.app/crypto"
"miniflux.app/logger"
"miniflux.app/model"

Expand Down Expand Up @@ -351,3 +352,35 @@ func (s *Storage) EntryURLExists(feedID int64, entryURL string) bool {
s.db.QueryRow(query, feedID, entryURL).Scan(&result)
return result
}

// GetEntryShareCode returns the share code of the provided entry.
// It generates a new one if not already defined.
func (s *Storage) GetEntryShareCode(userID int64, entryID int64) (shareCode string, err error) {
query := `SELECT share_code FROM entries WHERE user_id=$1 AND id=$2`
err = s.db.QueryRow(query, userID, entryID).Scan(&shareCode)

if err != nil || shareCode != "" {
return
}

shareCode = crypto.GenerateRandomStringHex(16)

query = `UPDATE entries SET share_code = $1 WHERE user_id=$2 AND id=$3`
result, err := s.db.Exec(query, shareCode, userID, entryID)
if err != nil {
err = fmt.Errorf(`store: unable to set share_code for entry #%d: %v`, entryID, err)
return
}

count, err := result.RowsAffected()
if err != nil {
err = fmt.Errorf(`store: unable to set share_code for entry #%d: %v`, entryID, err)
return
}

if count == 0 {
err = errors.New(`store: nothing has been updated`)
}

return
}
16 changes: 16 additions & 0 deletions storage/entry_query_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@ func (e *EntryQueryBuilder) WithoutStatus(status string) *EntryQueryBuilder {
return e
}

// WithShareCode set the entry hash.
func (e *EntryQueryBuilder) WithShareCode(shareCode string) *EntryQueryBuilder {
e.conditions = append(e.conditions, fmt.Sprintf("e.share_code = $%d", len(e.args)+1))
e.args = append(e.args, shareCode)
return e
}

// WithOrder set the sorting order.
func (e *EntryQueryBuilder) WithOrder(order string) *EntryQueryBuilder {
e.order = order
Expand Down Expand Up @@ -198,6 +205,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
e.url,
e.comments_url,
e.author,
e.share_code,
e.content,
e.status,
e.starred,
Expand Down Expand Up @@ -255,6 +263,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
&entry.URL,
&entry.CommentsURL,
&entry.Author,
&entry.ShareCode,
&entry.Content,
&entry.Status,
&entry.Starred,
Expand Down Expand Up @@ -358,3 +367,10 @@ func NewEntryQueryBuilder(store *Storage, userID int64) *EntryQueryBuilder {
conditions: []string{"e.user_id = $1"},
}
}

// NewAnonymousQueryBuilder returns a new EntryQueryBuilder suitable for anonymous users.
func NewAnonymousQueryBuilder(store *Storage) *EntryQueryBuilder {
return &EntryQueryBuilder{
store: store,
}
}
Loading

0 comments on commit 41a2b7e

Please sign in to comment.