Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da951164d5 | |||
| a0988f6c16 | |||
| fddc861e41 | |||
| 9288001f09 | |||
| da261a5fc7 | |||
| edeb5f0366 | |||
| 65ff328804 | |||
| 7759ea1b43 | |||
| 7c5d6cf35f | |||
| 39cc1887ea | |||
| b0a3b4d5d9 | |||
| 469f23968e |
@@ -1,3 +1,17 @@
|
||||
Version 2.2.3 (November 10, 2024)
|
||||
---------------------------------
|
||||
|
||||
* fix: unable to change password due to a typo in SQL parameter
|
||||
* fix: show only one player when there are several audio/video enclosures
|
||||
* feat(mediaproxy): pass original filename in `Content-Disposition` header
|
||||
* feat(mediaproxy): implement referer spoofing for restricted media resources
|
||||
* feat(integration): update Shiori integration to use new API endpoints for login/bookmark
|
||||
* build(deps): bump `golang.org/x/text` from `0.19.0` to `0.20.0`
|
||||
* build(deps): bump `golang.org/x/term` from `0.25.0` to `0.26.0`
|
||||
* build(deps): bump `golang.org/x/oauth2` from `0.23.0` to `0.24.0`
|
||||
* build(deps): bump `golang.org/x/net` from `0.30.0` to `0.31.0`
|
||||
* build(deps): bump `golang.org/x/crypto` from `0.28.0` to `0.29.0`
|
||||
|
||||
Version 2.2.2 (October 29, 2024)
|
||||
--------------------------------
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ require (
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/tdewolff/minify/v2 v2.21.1
|
||||
github.com/yuin/goldmark v1.7.8
|
||||
golang.org/x/crypto v0.28.0
|
||||
golang.org/x/net v0.30.0
|
||||
golang.org/x/oauth2 v0.23.0
|
||||
golang.org/x/term v0.25.0
|
||||
golang.org/x/text v0.19.0
|
||||
golang.org/x/crypto v0.29.0
|
||||
golang.org/x/net v0.31.0
|
||||
golang.org/x/oauth2 v0.24.0
|
||||
golang.org/x/term v0.26.0
|
||||
golang.org/x/text v0.20.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -41,7 +41,7 @@ require (
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.7.18 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -70,8 +70,8 @@ github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -79,10 +79,10 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -93,21 +93,21 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
||||
@@ -31,7 +31,7 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
return fmt.Errorf("shiori: missing base URL, username or password")
|
||||
}
|
||||
|
||||
sessionID, err := c.authenticate()
|
||||
token, err := c.authenticate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("shiori: unable to authenticate: %v", err)
|
||||
}
|
||||
@@ -44,7 +44,11 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
requestBody, err := json.Marshal(&addBookmarkRequest{
|
||||
URL: entryURL,
|
||||
Title: entryTitle,
|
||||
Excerpt: "",
|
||||
CreateArchive: true,
|
||||
CreateEbook: false,
|
||||
Public: 0,
|
||||
Tags: make([]string, 0),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -58,7 +62,7 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
request.Header.Set("X-Session-Id", sessionID)
|
||||
request.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
||||
|
||||
@@ -75,13 +79,13 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) authenticate() (sessionID string, err error) {
|
||||
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/login")
|
||||
func (c *Client) authenticate() (token string, err error) {
|
||||
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/v1/auth/login")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("shiori: invalid API endpoint: %v", err)
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(&authRequest{Username: c.username, Password: c.password})
|
||||
requestBody, err := json.Marshal(&authRequest{Username: c.username, Password: c.password, RememberMe: false})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("shiori: unable to encode request body: %v", err)
|
||||
}
|
||||
@@ -111,21 +115,31 @@ func (c *Client) authenticate() (sessionID string, err error) {
|
||||
if err := json.NewDecoder(response.Body).Decode(&authResponse); err != nil {
|
||||
return "", fmt.Errorf("shiori: unable to decode response: %v", err)
|
||||
}
|
||||
|
||||
return authResponse.SessionID, nil
|
||||
return authResponse.Message.Token, nil
|
||||
}
|
||||
|
||||
type authRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
RememberMe bool `json:"remember_me"`
|
||||
}
|
||||
|
||||
type authResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Message authResponseMessage `json:"message"`
|
||||
}
|
||||
|
||||
type authResponseMessage struct {
|
||||
SessionID string `json:"session"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type addBookmarkRequest struct {
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
CreateArchive bool `json:"createArchive"`
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
CreateArchive bool `json:"create_archive"`
|
||||
CreateEbook bool `json:"create_ebook"`
|
||||
Public int `json:"public"`
|
||||
Excerpt string `json:"excerpt"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
@@ -34,9 +35,34 @@ func (e Enclosure) Html5MimeType() string {
|
||||
return e.MimeType
|
||||
}
|
||||
|
||||
func (e *Enclosure) IsAudio() bool {
|
||||
return strings.HasPrefix(strings.ToLower(e.MimeType), "audio/")
|
||||
}
|
||||
|
||||
func (e *Enclosure) IsVideo() bool {
|
||||
return strings.HasPrefix(strings.ToLower(e.MimeType), "video/")
|
||||
}
|
||||
|
||||
func (e *Enclosure) IsImage() bool {
|
||||
mimeType := strings.ToLower(e.MimeType)
|
||||
mediaURL := strings.ToLower(e.URL)
|
||||
return strings.HasPrefix(mimeType, "image/") || strings.HasSuffix(mediaURL, ".jpg") || strings.HasSuffix(mediaURL, ".jpeg") || strings.HasSuffix(mediaURL, ".png") || strings.HasSuffix(mediaURL, ".gif")
|
||||
}
|
||||
|
||||
// EnclosureList represents a list of attachments.
|
||||
type EnclosureList []*Enclosure
|
||||
|
||||
// FindMediaPlayerEnclosure returns the first enclosure that can be played by a media player.
|
||||
func (el EnclosureList) FindMediaPlayerEnclosure() *Enclosure {
|
||||
for _, enclosure := range el {
|
||||
if enclosure.URL != "" && strings.Contains(enclosure.MimeType, "audio/") || strings.Contains(enclosure.MimeType, "video/") {
|
||||
return enclosure
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (el EnclosureList) ContainsAudioOrVideo() bool {
|
||||
for _, enclosure := range el {
|
||||
if strings.Contains(enclosure.MimeType, "audio/") || strings.Contains(enclosure.MimeType, "video/") {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
package rewrite // import "miniflux.app/v2/internal/reader/rewrite"
|
||||
|
||||
import "regexp"
|
||||
|
||||
// List of predefined rewrite rules (alphabetically sorted)
|
||||
// Available rules: "add_image_title", "add_youtube_video"
|
||||
// domain => rule name
|
||||
@@ -38,3 +40,42 @@ var predefinedRules = map[string]string{
|
||||
"xkcd.com": "add_image_title",
|
||||
"youtube.com": "add_youtube_video",
|
||||
}
|
||||
|
||||
type RefererRule struct {
|
||||
URLPattern *regexp.Regexp
|
||||
Referer string
|
||||
}
|
||||
|
||||
// List of predefined referer rules
|
||||
var PredefinedRefererRules = []RefererRule{
|
||||
{
|
||||
URLPattern: regexp.MustCompile(`^https://\w+\.sinaimg\.cn`),
|
||||
Referer: "https://weibo.com",
|
||||
},
|
||||
{
|
||||
URLPattern: regexp.MustCompile(`^https://i\.pximg\.net`),
|
||||
Referer: "https://www.pixiv.net",
|
||||
},
|
||||
{
|
||||
URLPattern: regexp.MustCompile(`^https://cdnfile\.sspai\.com`),
|
||||
Referer: "https://sspai.com",
|
||||
},
|
||||
{
|
||||
URLPattern: regexp.MustCompile(`^https://(?:\w|-)+\.cdninstagram\.com`),
|
||||
Referer: "https://www.instagram.com",
|
||||
},
|
||||
{
|
||||
URLPattern: regexp.MustCompile(`^https://sp1\.piokok\.com`),
|
||||
Referer: "https://sp1.piokok.com",
|
||||
},
|
||||
}
|
||||
|
||||
// GetRefererForURL returns the referer for the given URL if it exists, otherwise an empty string.
|
||||
func GetRefererForURL(url string) string {
|
||||
for _, rule := range PredefinedRefererRules {
|
||||
if rule.URLPattern.MatchString(url) {
|
||||
return rule.Referer
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
||||
custom_js=$14,
|
||||
external_font_hosts=$15,
|
||||
google_id=$16,
|
||||
openid_connect_id=$167,
|
||||
openid_connect_id=$17,
|
||||
display_mode=$18,
|
||||
entry_order=$19,
|
||||
default_reading_speed=$20,
|
||||
|
||||
@@ -165,56 +165,55 @@
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<article class="entry-content gesture-nav-{{ $.user.GestureNav }}" dir="auto">
|
||||
{{ if (and .entry.Enclosures (not .entry.Feed.NoMediaPlayer)) }}
|
||||
{{ range .entry.Enclosures }}
|
||||
{{ if ne .URL "" }}
|
||||
{{ if hasPrefix .MimeType "audio/" }}
|
||||
<div class="enclosure-audio" >
|
||||
<audio controls preload="metadata"
|
||||
{{ if $.user }}data-last-position="{{ .MediaProgression }}"{{ end }}
|
||||
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
||||
{{ if $.user.MarkReadOnMediaPlayerCompletion }}
|
||||
data-mark-read-on-completion="0.9"
|
||||
{{ end }}
|
||||
{{ if $.user }}data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"{{ end }}
|
||||
data-enclosure-id="{{.ID}}"
|
||||
>
|
||||
{{ if (and $.user (mustBeProxyfied "audio")) }}
|
||||
<source src="{{ proxyURL .URL }}" type="{{ .Html5MimeType }}">
|
||||
{{ else }}
|
||||
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
|
||||
{{ end }}
|
||||
</audio>
|
||||
{{ template "enclosure_media_controls" . }}
|
||||
</div>
|
||||
{{ else if hasPrefix .MimeType "video/" }}
|
||||
<div class="enclosure-video">
|
||||
<video controls preload="metadata"
|
||||
{{ if $.user }}data-last-position="{{ .MediaProgression }}"{{ end }}
|
||||
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
||||
{{ if $.user.MarkReadOnMediaPlayerCompletion }}
|
||||
data-mark-read-on-completion="0.9"
|
||||
{{ if not .entry.Feed.NoMediaPlayer }}
|
||||
{{ $mediaPlayerEnclosure := .entry.Enclosures.FindMediaPlayerEnclosure }}
|
||||
|
||||
{{ if $mediaPlayerEnclosure }}
|
||||
{{ with $mediaPlayerEnclosure }}
|
||||
{{ if .IsAudio }}
|
||||
<div class="enclosure-audio" >
|
||||
<audio controls preload="metadata"
|
||||
{{ if $.user }}data-last-position="{{ .MediaProgression }}"{{ end }}
|
||||
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
||||
{{ if $.user.MarkReadOnMediaPlayerCompletion }}data-mark-read-on-completion="0.9"{{ end }}
|
||||
{{ if $.user }}data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"{{ end }}
|
||||
data-enclosure-id="{{ .ID }}"
|
||||
>
|
||||
{{ if (and $.user (mustBeProxyfied "audio")) }}
|
||||
<source src="{{ proxyURL .URL }}" type="{{ .Html5MimeType }}">
|
||||
{{ else }}
|
||||
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
|
||||
{{ end }}
|
||||
</audio>
|
||||
{{ template "enclosure_media_controls" . }}
|
||||
</div>
|
||||
{{ else if .IsVideo }}
|
||||
<div class="enclosure-video">
|
||||
<video controls preload="metadata"
|
||||
{{ if $.user }}data-last-position="{{ .MediaProgression }}"{{ end }}
|
||||
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
||||
{{ if $.user.MarkReadOnMediaPlayerCompletion }}data-mark-read-on-completion="0.9"{{ end }}
|
||||
{{ if $.user }}data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"{{ end }}
|
||||
data-enclosure-id="{{ .ID }}"
|
||||
>
|
||||
{{ if (and $.user (mustBeProxyfied "video")) }}
|
||||
<source src="{{ proxyURL .URL }}" type="{{ .Html5MimeType }}">
|
||||
{{ else }}
|
||||
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
|
||||
{{ end }}
|
||||
</video>
|
||||
{{ template "enclosure_media_controls" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if $.user }}data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"{{ end }}
|
||||
data-enclosure-id="{{.ID}}"
|
||||
>
|
||||
{{ if (and $.user (mustBeProxyfied "video")) }}
|
||||
<source src="{{ proxyURL .URL }}" type="{{ .Html5MimeType }}">
|
||||
{{ else }}
|
||||
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
|
||||
{{ end }}
|
||||
</video>
|
||||
{{ template "enclosure_media_controls" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{end}}
|
||||
{{ if .user }}
|
||||
{{ end }}
|
||||
|
||||
{{ if .user }}
|
||||
{{ noescape (proxyFilter .entry.Content) }}
|
||||
{{ else }}
|
||||
{{ else }}
|
||||
{{ noescape .entry.Content }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</article>
|
||||
{{ if .entry.Enclosures }}
|
||||
<details class="entry-enclosures">
|
||||
@@ -222,45 +221,7 @@
|
||||
{{ range .entry.Enclosures }}
|
||||
{{ if ne .URL "" }}
|
||||
<div class="entry-enclosure">
|
||||
{{ if hasPrefix .MimeType "audio/" }}
|
||||
<div class="enclosure-audio">
|
||||
<audio controls preload="metadata"
|
||||
{{ if $.user }}data-last-position="{{ .MediaProgression }}"{{ end }}
|
||||
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
||||
{{ if $.user.MarkReadOnMediaPlayerCompletion }}
|
||||
data-mark-read-on-completion="0.9"
|
||||
{{ end }}
|
||||
{{ if $.user }}data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"{{ end }}
|
||||
data-enclosure-id="{{.ID}}"
|
||||
>
|
||||
{{ if (and $.user (mustBeProxyfied "audio")) }}
|
||||
<source src="{{ proxyURL .URL }}" type="{{ .Html5MimeType }}">
|
||||
{{ else }}
|
||||
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
|
||||
{{ end }}
|
||||
</audio>
|
||||
{{ template "enclosure_media_controls" . }}
|
||||
</div>
|
||||
{{ else if hasPrefix .MimeType "video/" }}
|
||||
<div class="enclosure-video">
|
||||
<video controls preload="metadata"
|
||||
{{ if $.user }}data-last-position="{{ .MediaProgression }}"{{ end }}
|
||||
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
||||
{{ if $.user.MarkReadOnMediaPlayerCompletion }}
|
||||
data-mark-read-on-completion="0.9"
|
||||
{{ end }}
|
||||
{{ if $.user }}data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"{{ end }}
|
||||
data-enclosure-id="{{.ID}}"
|
||||
>
|
||||
{{ if (and $.user (mustBeProxyfied "video")) }}
|
||||
<source src="{{ proxyURL .URL }}" type="{{ .Html5MimeType }}">
|
||||
{{ else }}
|
||||
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
|
||||
{{ end }}
|
||||
</video>
|
||||
{{ template "enclosure_media_controls" . }}
|
||||
</div>
|
||||
{{ else if hasPrefix .MimeType "image/" }}
|
||||
{{ if .IsImage }}
|
||||
<div class="enclosure-image">
|
||||
{{ if (and $.user (mustBeProxyfied "image")) }}
|
||||
<img src="{{ proxyURL .URL }}" title="{{ .URL }} ({{ .MimeType }})" loading="lazy" alt="{{ .URL }} ({{ .MimeType }})">
|
||||
@@ -271,7 +232,7 @@
|
||||
{{ end }}
|
||||
|
||||
<div class="entry-enclosure-download">
|
||||
<a href="{{ .URL | safeURL }}" title="{{ t "action.download" }}{{ if gt .Size 0 }} - {{ formatFileSize .Size }}{{ end }} ({{ .MimeType }})" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .URL | safeURL }}</a>
|
||||
<a href="{{ .URL | safeURL }}" title="{{ t "action.download" }}{{ if gt .Size 0 }} - {{ formatFileSize .Size }}{{ end }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .URL | safeURL }}</a>
|
||||
<small>{{ if gt .Size 0 }} - <strong>{{ formatFileSize .Size }}</strong>{{ end }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+15
-4
@@ -12,6 +12,7 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response"
|
||||
"miniflux.app/v2/internal/http/response/html"
|
||||
"miniflux.app/v2/internal/reader/rewrite"
|
||||
)
|
||||
|
||||
func (h *handler) mediaProxy(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -56,23 +58,23 @@ func (h *handler) mediaProxy(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
u, err := url.Parse(string(decodedURL))
|
||||
parsedMediaURL, err := url.Parse(string(decodedURL))
|
||||
if err != nil {
|
||||
html.BadRequest(w, r, errors.New("invalid URL provided"))
|
||||
return
|
||||
}
|
||||
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
if parsedMediaURL.Scheme != "http" && parsedMediaURL.Scheme != "https" {
|
||||
html.BadRequest(w, r, errors.New("invalid URL provided"))
|
||||
return
|
||||
}
|
||||
|
||||
if u.Host == "" {
|
||||
if parsedMediaURL.Host == "" {
|
||||
html.BadRequest(w, r, errors.New("invalid URL provided"))
|
||||
return
|
||||
}
|
||||
|
||||
if !u.IsAbs() {
|
||||
if !parsedMediaURL.IsAbs() {
|
||||
html.BadRequest(w, r, errors.New("invalid URL provided"))
|
||||
return
|
||||
}
|
||||
@@ -90,6 +92,10 @@ func (h *handler) mediaProxy(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
req.Header.Set("Connection", "close")
|
||||
|
||||
if referer := rewrite.GetRefererForURL(mediaURL); referer != "" {
|
||||
req.Header.Set("Referer", referer)
|
||||
}
|
||||
|
||||
forwardedRequestHeader := []string{"Range", "Accept", "Accept-Encoding", "User-Agent"}
|
||||
for _, requestHeaderName := range forwardedRequestHeader {
|
||||
if r.Header.Get(requestHeaderName) != "" {
|
||||
@@ -140,6 +146,11 @@ func (h *handler) mediaProxy(w http.ResponseWriter, r *http.Request) {
|
||||
b.WithStatus(resp.StatusCode)
|
||||
b.WithHeader("Content-Security-Policy", `default-src 'self'`)
|
||||
b.WithHeader("Content-Type", resp.Header.Get("Content-Type"))
|
||||
|
||||
if filename := path.Base(parsedMediaURL.Path); filename != "" {
|
||||
b.WithHeader("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, filename))
|
||||
}
|
||||
|
||||
forwardedResponseHeader := []string{"Content-Encoding", "Content-Type", "Content-Length", "Accept-Ranges", "Content-Range"}
|
||||
for _, responseHeaderName := range forwardedResponseHeader {
|
||||
if resp.Header.Get(responseHeaderName) != "" {
|
||||
|
||||
Reference in New Issue
Block a user