9 Commits

Author SHA1 Message Date
nkanaev
16a7f3409c youtube shorts in readability 2025-10-06 14:39:23 +01:00
nkanaev
0e11cec99a remove print statements 2025-10-06 14:39:23 +01:00
Nadia Santalla
c158912da4 fix media_links reading from DB
Prior to this commit, `MediaLinks` were always returned as `nil`.
Peeking a bit I figured that's becuase the argument to `MediaLinks.Scan`
is in fact a string, and not a `[]byte` as the code expects. I guess
that might be because `media_links` is a `json` (not `jsonb`) column in
sqlite. I have no idea which of the two is best to use for the DB side,
but it's easy to make the code support both.
2025-10-06 14:18:03 +01:00
nkanaev
08ad04401d Update changelog.md 2025-10-02 19:31:37 +01:00
nkanaev
a851d8ac9d minor ui tweaks 2025-10-02 19:31:37 +01:00
Your Name
5a3547e32e host build for openbsd 2025-10-02 10:26:44 +01:00
Your Name
b24152c19a fix mustHideFolder 2025-10-02 10:23:29 +01:00
nkanaev
9f93298cf9 restrict private IP access 2025-10-02 10:16:35 +01:00
Adam Simpson
ac9b635ed8 app: add support for theme-color
I use the "web app" version of yarr on my iPhone and the area around the
notch/island is un-themed.

Using [theme-color][1] we can control that color.

[1]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta/name/theme-color
2025-09-25 14:28:15 +01:00
11 changed files with 101 additions and 9 deletions

View File

@@ -3,7 +3,10 @@
- (new) serve on unix socket (thanks to @rvighne)
- (new) more auto-refresh options: 12h & 24h (thanks to @aswerkljh for suggestion)
- (fix) smooth scrolling on iOS (thanks to gatheraled)
- (fix) displaying youtube shorts in "Read Here" (thanks to @Dean-Corso for the report)
- (etc) theme-color support (thanks to @asimpson)
- (etc) cookie security measures (thanks to Tom Fitzhenry)
- (etc) restrict access to internal IPs for page crawler (thanks to Omar Kurt)
# v2.5 (2025-03-26)

View File

@@ -5,6 +5,7 @@ GO_TAGS = sqlite_foreign_keys sqlite_json
GO_LDFLAGS = -s -w -X 'main.Version=$(VERSION)' -X 'main.GitHash=$(GITHASH)'
GO_FLAGS = -tags "$(GO_TAGS)" -ldflags="$(GO_LDFLAGS)"
GO_FLAGS_DEBUG = -tags "$(GO_TAGS) debug"
GO_FLAGS_GUI = -tags "$(GO_TAGS) gui" -ldflags="$(GO_LDFLAGS)"
GO_FLAGS_GUI_WIN = -tags "$(GO_TAGS) gui" -ldflags="$(GO_LDFLAGS) -H windowsgui"
@@ -75,7 +76,7 @@ windows_arm64_gui: src/platform/versioninfo.rc
GOOS=windows GOARCH=arm64 go build $(GO_FLAGS_GUI_WIN) -o out/$@/yarr.exe ./cmd/yarr
serve:
go run $(GO_FLAGS) ./cmd/yarr -db local.db
go run $(GO_FLAGS_DEBUG) ./cmd/yarr -db local.db
test:
go test $(GO_FLAGS) ./...

View File

@@ -1,3 +1,5 @@
//go:build !debug
package assets
import "embed"

View File

@@ -8,6 +8,7 @@
<link rel="icon" href="./static/graphicarts/favicon.svg" type="image/svg+xml">
<link rel="alternate icon" href="./static/graphicarts/favicon.png" type="image/png">
<link rel="manifest" href="./manifest.json" />
<meta name="theme-color" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<script>
window.app = window.app || {}
@@ -23,21 +24,21 @@
<div class="p-2 toolbar d-flex align-items-center">
<div class="icon mx-2">{% inline "anchor.svg" %}</div>
<div class="flex-grow-1"></div>
<button class="toolbar-item"
<button class="toolbar-item ml-1"
:class="{active: filterSelected == 'unread'}"
:aria-pressed="filterSelected == 'unread'"
title="Unread"
@click="filterSelected = 'unread'">
<span class="icon">{% inline "circle-full.svg" %}</span>
</button>
<button class="toolbar-item"
<button class="toolbar-item mx-1"
:class="{active: filterSelected == 'starred'}"
:aria-pressed="filterSelected == 'starred'"
title="Starred"
@click="filterSelected = 'starred'">
<span class="icon">{% inline "star-full.svg" %}</span>
</button>
<button class="toolbar-item"
<button class="toolbar-item mr-1"
:class="{active: filterSelected == ''}"
:aria-pressed="filterSelected == ''"
title="All"

View File

@@ -211,6 +211,7 @@ var vm = new Vue({
api.feeds.list_errors().then(function(errors) {
vm.feed_errors = errors
})
this.updateMetaTheme(app.settings.theme_name)
},
data: function() {
var s = app.settings
@@ -249,6 +250,11 @@ var vm = new Vue({
'font': s.theme_font,
'size': s.theme_size,
},
'themeColors': {
'night': '#0e0e0e',
'sepia': '#f4f0e5',
'light': '#fff',
},
'refreshRate': s.refresh_rate,
'authenticated': app.authenticated,
'feed_errors': {},
@@ -330,6 +336,7 @@ var vm = new Vue({
'theme': {
deep: true,
handler: function(theme) {
this.updateMetaTheme(theme.name)
document.body.classList.value = 'theme-' + theme.name
api.settings.update({
theme_name: theme.name,
@@ -405,6 +412,9 @@ var vm = new Vue({
},
},
methods: {
updateMetaTheme: function(theme) {
document.querySelector("meta[name='theme-color']").content = this.themeColors[theme]
},
refreshStats: function(loopMode) {
return api.status().then(function(data) {
if (loopMode && !vm.itemSelected) vm.refreshItems()
@@ -810,7 +820,7 @@ var vm = new Vue({
return this.filterSelected
&& !(this.current.folder.id == folder.id || this.current.feed.folder_id == folder.id)
&& !this.filteredFolderStats[folder.id]
&& (!this.itemSelectedDetails || (this.feedsById[itemSelectedDetails.feed_id] || {}).folder_id != folder.id)
&& (!this.itemSelectedDetails || (this.feedsById[this.itemSelectedDetails.feed_id] || {}).folder_id != folder.id)
},
mustHideFeed: function (feed) {
return this.filterSelected

View File

@@ -22,6 +22,8 @@ func VideoIFrame(link string) string {
youtubeID := ""
if l.Host == "www.youtube.com" && l.Path == "/watch" {
youtubeID = l.Query().Get("v")
} else if l.Host == "www.youtube.com" && strings.HasPrefix(l.Path, "/shorts/") {
youtubeID = strings.TrimPrefix(l.Path, "/shorts/")
} else if l.Host == "youtu.be" {
youtubeID = strings.TrimLeft(l.Path, "/")
}

View File

@@ -1,4 +1,4 @@
//go:build linux || freebsd
//go:build linux || freebsd || openbsd
package platform

View File

@@ -513,6 +513,10 @@ func (s *Server) handlePageCrawl(c *router.Context) {
})
return
}
if isInternalFromURL(url) {
log.Printf("attempt to access internal IP %s from %s", url, c.Req.RemoteAddr)
return
}
body, err := worker.GetBody(url)
if err != nil {

35
src/server/util.go Normal file
View File

@@ -0,0 +1,35 @@
package server
import (
"net"
"net/url"
"strings"
)
func isInternalFromURL(urlStr string) bool {
parsedURL, err := url.Parse(urlStr)
if err != nil {
return false
}
host := parsedURL.Host
// Handle "host:port" format
if strings.Contains(host, ":") {
host, _, err = net.SplitHostPort(host)
if err != nil {
return false
}
}
if host == "localhost" {
return true
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast()
}

31
src/server/util_test.go Normal file
View File

@@ -0,0 +1,31 @@
package server
import "testing"
func TestIsInternalFromURL(t *testing.T) {
tests := []struct {
url string
expected bool
}{
{"http://192.168.1.1:8080", true},
{"http://10.0.0.5", true},
{"http://172.16.0.1", true},
{"http://172.31.255.255", true},
{"http://172.32.0.1", false}, // outside private range
{"http://127.0.0.1", true},
{"http://127.0.0.1:7000", true},
{"http://127.0.0.1:7000/secret", true},
{"http://169.254.0.5", true},
{"http://localhost", true}, // resolves to 127.0.0.1
{"http://8.8.8.8", false},
{"http://google.com", false}, // resolves to public IPs
{"invalid-url", false}, // invalid format
{"", false}, // empty string
}
for _, test := range tests {
result := isInternalFromURL(test.url)
if result != test.expected {
t.Errorf("isInternalFromURL(%q) = %v; want %v", test.url, result, test.expected)
}
}
}

View File

@@ -54,11 +54,15 @@ type MediaLink struct {
type MediaLinks []MediaLink
func (m *MediaLinks) Scan(src any) error {
if data, ok := src.([]byte); ok {
switch data := src.(type) {
case []byte:
return json.Unmarshal(data, m)
}
case string:
return json.Unmarshal([]byte(data), m)
default:
return nil
}
}
func (m MediaLinks) Value() (driver.Value, error) {
return json.Marshal(m)
@@ -419,7 +423,6 @@ func (s *Storage) DeleteOldItems() {
where status != ?
group by i.feed_id
`, itemsKeepSize, STARRED)
if err != nil {
log.Print(err)
return