Skip to content

Commit

Permalink
Add subscription manager, fix corrupt read/write & short URI panic
Browse files Browse the repository at this point in the history
More info on corrupt reads and writes:
#61 (comment)

The panic was occuring due to lines like:

    if u[:6] == "about:"

These would cause panics for URIs that were shorter than 6, like "docs/".
strings.HasPrefix was used instead to fix this.
  • Loading branch information
makew0rld committed Dec 6, 2020
1 parent 62102d4 commit 1a2fba9
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 41 deletions.
2 changes: 1 addition & 1 deletion NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@
- Bookmark keys aren't deleted, just set to `""`
- Waiting on [this viper PR](https://github.com/spf13/viper/pull/519) to be merged
- Help table cells aren't dynamically wrapped
- Filed [issue 29](https://gitlab.com/tslocum/cview/-/issues/29)
- Filed [issue 29](https://gitlab.com/tslocum/cview/-/issues/29)
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,9 @@ Amfora ❤️ open source!
- It uses [tcell](https://github.com/gdamore/tcell) for low level terminal operations
- [Viper](https://github.com/spf13/viper) for configuration and TOFU storing
- [go-gemini](https://github.com/makeworld-the-better-one/go-gemini), my forked and updated Gemini client/server library
- My [progressbar fork](https://github.com/makeworld-the-better-one/progressbar)
- My [progressbar fork](https://github.com/makeworld-the-better-one/progressbar) - pull request [here](https://github.com/schollz/progressbar/pull/69)
- [go-humanize](https://github.com/dustin/go-humanize)
- My [gofeed fork](https://github.com/makeworld-the-better-one/gofeed) - pull request [here](https://github.com/mmcdole/gofeed/pull/164)

## License
This project is licensed under the GPL v3.0. See the [LICENSE](./LICENSE) file for details.
9 changes: 6 additions & 3 deletions display/display.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,16 +576,19 @@ func Reload() {
// URL loads and handles the provided URL for the current tab.
// It should be an absolute URL.
func URL(u string) {
if u[:6] == "about:" {
handleAbout(tabs[curTab], u)
t := tabs[curTab]
if strings.HasPrefix(u, "about:") {
if ok := handleAbout(t, u); ok {
t.addToHistory(u)
}
return
}

if !strings.HasPrefix(u, "//") && !strings.HasPrefix(u, "gemini://") && !strings.Contains(u, "://") {
// Assume it's a Gemini URL
u = "gemini://" + u
}
go goURL(tabs[curTab], u)
go goURL(t, u)
}

func NumTabs() int {
Expand Down
15 changes: 11 additions & 4 deletions display/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,27 +167,34 @@ func handleFavicon(t *tab, host, old string) {
// 'about:'. It will display errors if the URL is not recognized,
// but not display anything if an 'about:' URL is not passed.
//
// It does not add the displayed page to history.
//
// It returns a bool indicating if the provided URL could be handled.
func handleAbout(t *tab, u string) bool {
if u[:6] != "about:" {
if !strings.HasPrefix(u, "about:") {
return false
}

switch u {
case "about:bookmarks":
Bookmarks(t)
t.addToHistory("about:bookmarks")
return true
case "about:subscriptions":
Subscriptions(t)
t.addToHistory("about:subscriptions")
return true
case "about:newtab":
temp := newTabPage // Copy
setPage(t, &temp)
t.applyBottomBar()
return true
}

if u == "about:manage-subscriptions" || (len(u) > 27 && u[:27] == "about:manage-subscriptions?") {
ManageSubscriptions(t, u)
// Don't count remove command in history
return u == "about:manage-subscriptions"
}

Error("Error", "Not a valid 'about:' URL.")
return false
}
Expand Down Expand Up @@ -244,7 +251,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {

App.SetFocus(t.view)

if u[:6] == "about:" {
if strings.HasPrefix(u, "about:") {
return ret(u, handleAbout(t, u))
}

Expand Down
2 changes: 1 addition & 1 deletion display/newtab.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ You can customize this page by creating a gemtext file called newtab.gmi, in Amf
Happy browsing!
=> about:bookmarks Bookmarks
=> about:subscriptions Feed and Page Tracking
=> about:subscriptions Subscriptions
=> //gemini.circumlunar.space Project Gemini
=> https://github.com/makeworld-the-better-one/amfora Amfora homepage [HTTPS]
Expand Down
6 changes: 4 additions & 2 deletions display/private.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import (
// Not when a URL is opened on a new tab for the first time.
// It will handle setting the bottomBar.
func followLink(t *tab, prev, next string) {
if next[:6] == "about:" {
handleAbout(t, next)
if strings.HasPrefix(next, "about:") {
if ok := handleAbout(t, next); ok {
t.addToHistory(next)
}
return
}

Expand Down
69 changes: 61 additions & 8 deletions display/subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/makeworld-the-better-one/amfora/renderer"
"github.com/makeworld-the-better-one/amfora/structs"
"github.com/makeworld-the-better-one/amfora/subscriptions"
"github.com/makeworld-the-better-one/go-gemini"
"github.com/mmcdole/gofeed"
"github.com/spf13/viper"
)
Expand All @@ -28,7 +29,7 @@ func toLocalDay(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
}

// Feeds displays the feeds page on the current tab.
// Subscriptions displays the subscriptions page on the current tab.
func Subscriptions(t *tab) {
logger.Log.Println("display.Subscriptions called")

Expand All @@ -43,9 +44,10 @@ func Subscriptions(t *tab) {

logger.Log.Println("started rendering subscriptions page")

subscriptionPageRaw := "# Subscriptions\n\n" +
rawPage := "# Subscriptions\n\n" +
"See the help (by pressing ?) for details on how to use this page.\n\n" +
"If you just opened Amfora then updates will appear incrementally. Reload the page to see them.\n"
"If you just opened Amfora then updates will appear incrementally. Reload the page to see them.\n\n" +
"=> about:manage-subscriptions Manage subscriptions\n"

// curDay represents what day of posts the loop is on.
// It only goes backwards in time.
Expand All @@ -67,21 +69,21 @@ func Subscriptions(t *tab) {
if pub.Before(curDay) {
// This post is on a new day, add a day header
curDay = pub
subscriptionPageRaw += fmt.Sprintf("\n## %s\n\n", curDay.Format("Jan 02, 2006"))
rawPage += fmt.Sprintf("\n## %s\n\n", curDay.Format("Jan 02, 2006"))
}
if entry.Title == "" || entry.Title == "/" {
// Just put author/title
// Mainly used for when you're tracking the root domain of a site
subscriptionPageRaw += fmt.Sprintf("=>%s %s\n", entry.URL, entry.Prefix)
rawPage += fmt.Sprintf("=>%s %s\n", entry.URL, entry.Prefix)
} else {
// Include title and dash
subscriptionPageRaw += fmt.Sprintf("=>%s %s - %s\n", entry.URL, entry.Prefix, entry.Title)
rawPage += fmt.Sprintf("=>%s %s - %s\n", entry.URL, entry.Prefix, entry.Title)
}
}

content, links := renderer.RenderGemini(subscriptionPageRaw, textWidth(), leftMargin(), false)
content, links := renderer.RenderGemini(rawPage, textWidth(), leftMargin(), false)
page := structs.Page{
Raw: subscriptionPageRaw,
Raw: rawPage,
Content: content,
Links: links,
URL: "about:subscriptions",
Expand All @@ -97,6 +99,57 @@ func Subscriptions(t *tab) {
logger.Log.Println("done rendering subscriptions page")
}

// ManageSubscriptions displays the subscription managing page in
// the current tab. `u` is the URL entered by the user.
func ManageSubscriptions(t *tab, u string) {
if len(u) > 27 && u[:27] == "about:manage-subscriptions?" {
// There's a query string, aka a URL to unsubscribe from
manageSubscriptionQuery(t, u)
return
}

rawPage := "# Manage Subscriptions\n\n" +
"Below is list of URLs, both feeds and pages. Navigate to the link to unsubscribe from that feed or page.\n\n"

for _, u2 := range subscriptions.AllURLS() {
rawPage += fmt.Sprintf(
"=>%s %s\n",
"about:manage-subscriptions?"+gemini.QueryEscape(u2),
u2,
)
}

content, links := renderer.RenderGemini(rawPage, textWidth(), leftMargin(), false)
page := structs.Page{
Raw: rawPage,
Content: content,
Links: links,
URL: "about:manage-subscriptions",
Width: termW,
Mediatype: structs.TextGemini,
}
go cache.AddPage(&page)
setPage(t, &page)
t.applyBottomBar()
}

func manageSubscriptionQuery(t *tab, u string) {
sub, err := gemini.QueryUnescape(u[27:])
if err != nil {
Error("URL Error", "Invalid query string: "+err.Error())
return
}

err = subscriptions.Remove(sub)
if err != nil {
ManageSubscriptions(t, "about:manage-subscriptions") // Reload
Error("Save Error", "Error saving the unsubscription to disk: "+err.Error())
return
}
ManageSubscriptions(t, "about:manage-subscriptions") // Reload
Info("Unsubscribed from " + sub)
}

// openSubscriptionModal displays the "Add subscription" modal
// It returns whether the user wanted to subscribe to feed/page.
// The subscribed arg specifies whether this feed/page is already
Expand Down
5 changes: 2 additions & 3 deletions display/tab.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,8 @@ func (t *tab) pageDown() {
t.view.ScrollTo(row+(termH/4)*3, col)
}

// hasContent returns true when the tab has a page that could be displayed.
// The most likely situation where false would be returned is when the default
// new tab content is being displayed.
// hasContent returns false when the tab's page is malformed,
// has no content or URL, or if it's an 'about:' page.
func (t *tab) hasContent() bool {
if t.page == nil || t.view == nil {
return false
Expand Down
3 changes: 1 addition & 2 deletions subscriptions/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ type pageJSON struct {
var data = jsonData{
feedMu: &sync.RWMutex{},
pageMu: &sync.RWMutex{},
Feeds: make(map[string]*gofeed.Feed),
Pages: make(map[string]*pageJSON),
// Maps are created in Init()
}

// PageEntry is a single item on a subscriptions page.
Expand Down
72 changes: 56 additions & 16 deletions subscriptions/subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"mime"
"os"
"path"
"reflect"
"sort"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -42,20 +44,29 @@ func Init() error {
f, err := os.Open(config.SubscriptionPath)
if err == nil {
// File exists and could be opened
defer f.Close()

fi, err := f.Stat()
if err == nil && fi.Size() > 0 {
// File is not empty
dec := json.NewDecoder(f)
err = dec.Decode(&data)
if err != nil && err != io.EOF {

jsonBytes, err := ioutil.ReadAll(f)
f.Close()
if err != nil {
return fmt.Errorf("read subscriptions.json error: %w", err)
}
err = json.Unmarshal(jsonBytes, &data)
if err != nil {
return fmt.Errorf("subscriptions.json is corrupted: %w", err) //nolint:goerr113
}
}
f.Close()
} else if !os.IsNotExist(err) {
// There's an error opening the file, but it's not bc is doesn't exist
return fmt.Errorf("open subscriptions.json error: %w", err) //nolint:goerr113
} else {
// File does not exist, initialize maps
data.Feeds = make(map[string]*gofeed.Feed)
data.Pages = make(map[string]*pageJSON)
}

LastUpdated = time.Now()
Expand Down Expand Up @@ -130,26 +141,21 @@ func writeJSON() error {
writeMu.Lock()
defer writeMu.Unlock()

f, err := os.OpenFile(config.SubscriptionPath, os.O_WRONLY|os.O_CREATE, 0666)
data.Lock()
logger.Log.Println("subscriptions.writeJSON acquired data lock")
jsonBytes, err := json.MarshalIndent(&data, "", " ")
data.Unlock()
if err != nil {
logger.Log.Println("subscriptions.writeJSON error", err)
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")

data.Lock()
logger.Log.Println("subscriptions.writeJSON acquired data lock")
err = enc.Encode(&data)
data.Unlock()

err = ioutil.WriteFile(config.SubscriptionPath, jsonBytes, 0666)
if err != nil {
logger.Log.Println("subscriptions.writeJSON error", err)
return err
}

return err
return nil
}

// AddFeed stores a feed.
Expand Down Expand Up @@ -363,3 +369,37 @@ func updateAll() {

wg.Wait()
}

// AllURLs returns all the subscribed-to URLS, sorted alphabetically.
func AllURLS() []string {
data.RLock()
defer data.RUnlock()

urls := make([]string, len(data.Feeds)+len(data.Pages))
i := 0
for k := range data.Feeds {
urls[i] = k
i++
}
for k := range data.Pages {
urls[i] = k
i++
}

sort.Strings(urls)
return urls
}

// Remove removes a subscription from memory and from the disk.
// The URL must be provided. It will do nothing if the URL is
// not an actual subscription.
//
// It returns any errors that occured when saving to disk.
func Remove(u string) error {
data.Lock()
// Just delete from both instead of using a loop to find it
delete(data.Feeds, u)
delete(data.Pages, u)
data.Unlock()
return writeJSON()
}

0 comments on commit 1a2fba9

Please sign in to comment.