Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 77cafe9c2c | |||
| 3b39f0883c | |||
| cf7a7e25fb | |||
| fb49ad24d5 | |||
| b270159aae | |||
| 44decae557 | |||
| 7f2fd1fdd8 | |||
| 0f3f5e442f | |||
| f19ab21b7d | |||
| ff8e0c6b3d | |||
| b0667e1b78 | |||
| 9554d0bd53 | |||
| e81e4f0ac3 | |||
| 4674d559a4 | |||
| 0c2e5ff0dc | |||
| 39c4452142 | |||
| 2f4cd59ad9 | |||
| 5cacae6cf2 | |||
| 7a1653a2e9 | |||
| 31da4db14f | |||
| b166ceaea7 | |||
| f49b42f70f | |||
| 1eba1730d1 | |||
| 04adf5fdf5 | |||
| ddd3af4b85 | |||
| 6b360d08c1 | |||
| 322b265d7a | |||
| 920dda79b7 |
@@ -1,3 +1,33 @@
|
||||
Version 2.0.8 (June 4, 2018)
|
||||
----------------------------
|
||||
|
||||
* Add Pocket integration
|
||||
* Rewrite RealIP() to avoid returning an empty string
|
||||
* Convert IP field from text to inet type
|
||||
* Improve error handling in integration clients
|
||||
* Make unread counter clickable
|
||||
* Archive read entries automatically after 60 days
|
||||
* Hide horizontal overflow when swiping articles on touch devices
|
||||
* Add API endpoint to get logged user
|
||||
* Fever API: Return response with an empty list if there is no item
|
||||
* Handle feeds with dates formatted as Unix timestamp
|
||||
|
||||
Version 2.0.7 (May 7, 2018)
|
||||
---------------------------
|
||||
|
||||
* Add API endpoint to import OPML file
|
||||
* Make sure to close request body in HTTP client
|
||||
* Do not show save link if no integration is configured
|
||||
* Make sure integrations are configured before to make any HTTP requests
|
||||
* Avoid people to unlink their OAuth2 account without having a local password
|
||||
* Do not use shared variable to translate templates (avoid concurrency issue)
|
||||
* Use vanilla HTTP handlers (refactoring)
|
||||
* Move HTTP client to its own package (refactoring)
|
||||
* Add middleware to read X-Forwarded-Proto header (refactoring)
|
||||
* Use Gorilla middleware (refactoring)
|
||||
* Scrape parent element for iframe
|
||||
* Add SoundCloud and Bandcamp iframe sources
|
||||
|
||||
Version 2.0.6 (Apr 20, 2018)
|
||||
----------------------------
|
||||
|
||||
|
||||
Generated
+4
-10
@@ -28,8 +28,8 @@
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/mux"
|
||||
packages = ["."]
|
||||
revision = "7f08801859139f86dfafd1c296e2cba9a80d292e"
|
||||
version = "v1.6.0"
|
||||
revision = "53c1911da2b537f792e7cafcb446b05ffe33b996"
|
||||
version = "v1.6.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -45,7 +45,7 @@
|
||||
branch = "master"
|
||||
name = "github.com/miniflux/miniflux-go"
|
||||
packages = ["."]
|
||||
revision = "887ba3b062946784f0e64edb1734f435beb204f9"
|
||||
revision = "8863d558cbf1f20beeb640328829205971f1a632"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/tdewolff/minify"
|
||||
@@ -69,12 +69,6 @@
|
||||
revision = "639f6272aec6b52094db77b9ec488214b0b4b1a1"
|
||||
version = "v2.3.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/tomasen/realip"
|
||||
packages = ["."]
|
||||
revision = "b5850897b7b539a1c9f22cdaa3b547d1bd453db8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
@@ -158,6 +152,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "338222e5111416c46b2b8bde149443abc542b386dd02aff2a0dd6e13334bcf28"
|
||||
inputs-digest = "f030af166f90ac96d133b7ddf9f1f80785508462cac8cae4c65abbdb7c20474b"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
+1
-5
@@ -31,7 +31,7 @@
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/mux"
|
||||
version = "1.6.0"
|
||||
version = "1.6.1"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
@@ -45,10 +45,6 @@
|
||||
name = "github.com/tdewolff/minify"
|
||||
version = "2.3.3"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/tomasen/realip"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
|
||||
@@ -25,7 +25,8 @@ The Miniflux documentation is available here: <https://docs.miniflux.net/>
|
||||
- [Opinionated?](https://docs.miniflux.net/en/latest/opinionated.html)
|
||||
- [Features](https://docs.miniflux.net/en/latest/features.html)
|
||||
- [Requirements](https://docs.miniflux.net/en/latest/requirements.html)
|
||||
- [Installation](https://docs.miniflux.net/en/latest/installation.html)
|
||||
- [Installation Instructions](https://docs.miniflux.net/en/latest/installation.html)
|
||||
- [Installation Tutorials](https://docs.miniflux.net/en/latest/tutorials.html)
|
||||
- [Upgrading to a new version](https://docs.miniflux.net/en/latest/upgrade.html)
|
||||
- [Configuration](https://docs.miniflux.net/en/latest/configuration.html)
|
||||
|
||||
|
||||
+33
-26
@@ -6,98 +6,105 @@ package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/json"
|
||||
)
|
||||
|
||||
// CreateCategory is the API handler to create a new category.
|
||||
func (c *Controller) CreateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
category, err := decodeCategoryPayload(request.Body())
|
||||
func (c *Controller) CreateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
category, err := decodeCategoryPayload(r.Body)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
category.UserID = userID
|
||||
if err := category.ValidateCategoryCreation(); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if c, err := c.store.CategoryByTitle(userID, category.Title); err != nil || c != nil {
|
||||
response.JSON().BadRequest(errors.New("This category already exists"))
|
||||
json.BadRequest(w, errors.New("This category already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.CreateCategory(category)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to create this category"))
|
||||
json.ServerError(w, errors.New("Unable to create this category"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Created(category)
|
||||
json.Created(w, category)
|
||||
}
|
||||
|
||||
// UpdateCategory is the API handler to update a category.
|
||||
func (c *Controller) UpdateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
categoryID, err := request.IntegerParam("categoryID")
|
||||
func (c *Controller) UpdateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
categoryID, err := request.IntParam(r, "categoryID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := decodeCategoryPayload(request.Body())
|
||||
category, err := decodeCategoryPayload(r.Body)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.New(r)
|
||||
category.UserID = ctx.UserID()
|
||||
category.ID = categoryID
|
||||
if err := category.ValidateCategoryModification(); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.UpdateCategory(category)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to update this category"))
|
||||
json.ServerError(w, errors.New("Unable to update this category"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Created(category)
|
||||
json.Created(w, category)
|
||||
}
|
||||
|
||||
// GetCategories is the API handler to get a list of categories for a given user.
|
||||
func (c *Controller) GetCategories(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) GetCategories(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
categories, err := c.store.Categories(ctx.UserID())
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch categories"))
|
||||
json.ServerError(w, errors.New("Unable to fetch categories"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(categories)
|
||||
json.OK(w, categories)
|
||||
}
|
||||
|
||||
// RemoveCategory is the API handler to remove a category.
|
||||
func (c *Controller) RemoveCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) RemoveCategory(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
categoryID, err := request.IntegerParam("categoryID")
|
||||
categoryID, err := request.IntParam(r, "categoryID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.store.CategoryExists(userID, categoryID) {
|
||||
response.JSON().NotFound(errors.New("Category not found"))
|
||||
json.NotFound(w, errors.New("Category not found"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.RemoveCategory(userID, categoryID); err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to remove this category"))
|
||||
json.ServerError(w, errors.New("Unable to remove this category"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().NoContent()
|
||||
json.NoContent(w)
|
||||
}
|
||||
|
||||
+65
-67
@@ -6,107 +6,110 @@ package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/json"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
)
|
||||
|
||||
// GetFeedEntry is the API handler to get a single feed entry.
|
||||
func (c *Controller) GetFeedEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
func (c *Controller) GetFeedEntry(w http.ResponseWriter, r *http.Request) {
|
||||
feedID, err := request.IntParam(r, "feedID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
entryID, err := request.IntParam(r, "entryID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithFeedID(feedID)
|
||||
builder.WithEntryID(entryID)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch this entry from the database"))
|
||||
json.ServerError(w, errors.New("Unable to fetch this entry from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
response.JSON().NotFound(errors.New("Entry not found"))
|
||||
json.NotFound(w, errors.New("Entry not found"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(entry)
|
||||
json.OK(w, entry)
|
||||
}
|
||||
|
||||
// GetEntry is the API handler to get a single entry.
|
||||
func (c *Controller) GetEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
func (c *Controller) GetEntry(w http.ResponseWriter, r *http.Request) {
|
||||
entryID, err := request.IntParam(r, "entryID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(userID)
|
||||
builder := c.store.NewEntryQueryBuilder(context.New(r).UserID())
|
||||
builder.WithEntryID(entryID)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch this entry from the database"))
|
||||
json.ServerError(w, errors.New("Unable to fetch this entry from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
response.JSON().NotFound(errors.New("Entry not found"))
|
||||
json.NotFound(w, errors.New("Entry not found"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(entry)
|
||||
json.OK(w, entry)
|
||||
}
|
||||
|
||||
// GetFeedEntries is the API handler to get all feed entries.
|
||||
func (c *Controller) GetFeedEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
func (c *Controller) GetFeedEntries(w http.ResponseWriter, r *http.Request) {
|
||||
feedID, err := request.IntParam(r, "feedID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
status := request.QueryStringParam("status", "")
|
||||
status := request.QueryParam(r, "status", "")
|
||||
if status != "" {
|
||||
if err := model.ValidateEntryStatus(status); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
order := request.QueryStringParam("order", model.DefaultSortingOrder)
|
||||
order := request.QueryParam(r, "order", model.DefaultSortingOrder)
|
||||
if err := model.ValidateEntryOrder(order); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
direction := request.QueryStringParam("direction", model.DefaultSortingDirection)
|
||||
direction := request.QueryParam(r, "direction", model.DefaultSortingDirection)
|
||||
if err := model.ValidateDirection(direction); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
limit := request.QueryIntegerParam("limit", 100)
|
||||
offset := request.QueryIntegerParam("offset", 0)
|
||||
limit := request.QueryIntParam(r, "limit", 100)
|
||||
offset := request.QueryIntParam(r, "offset", 0)
|
||||
if err := model.ValidateRange(offset, limit); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(userID)
|
||||
builder := c.store.NewEntryQueryBuilder(context.New(r).UserID())
|
||||
builder.WithFeedID(feedID)
|
||||
builder.WithStatus(status)
|
||||
builder.WithOrder(order)
|
||||
@@ -116,51 +119,49 @@ func (c *Controller) GetFeedEntries(ctx *handler.Context, request *handler.Reque
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch the list of entries"))
|
||||
json.ServerError(w, errors.New("Unable to fetch the list of entries"))
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to count the number of entries"))
|
||||
json.ServerError(w, errors.New("Unable to count the number of entries"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(&entriesResponse{Total: count, Entries: entries})
|
||||
json.OK(w, &entriesResponse{Total: count, Entries: entries})
|
||||
}
|
||||
|
||||
// GetEntries is the API handler to fetch entries.
|
||||
func (c *Controller) GetEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
|
||||
status := request.QueryStringParam("status", "")
|
||||
func (c *Controller) GetEntries(w http.ResponseWriter, r *http.Request) {
|
||||
status := request.QueryParam(r, "status", "")
|
||||
if status != "" {
|
||||
if err := model.ValidateEntryStatus(status); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
order := request.QueryStringParam("order", model.DefaultSortingOrder)
|
||||
order := request.QueryParam(r, "order", model.DefaultSortingOrder)
|
||||
if err := model.ValidateEntryOrder(order); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
direction := request.QueryStringParam("direction", model.DefaultSortingDirection)
|
||||
direction := request.QueryParam(r, "direction", model.DefaultSortingDirection)
|
||||
if err := model.ValidateDirection(direction); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
limit := request.QueryIntegerParam("limit", 100)
|
||||
offset := request.QueryIntegerParam("offset", 0)
|
||||
limit := request.QueryIntParam(r, "limit", 100)
|
||||
offset := request.QueryIntParam(r, "offset", 0)
|
||||
if err := model.ValidateRange(offset, limit); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(userID)
|
||||
builder := c.store.NewEntryQueryBuilder(context.New(r).UserID())
|
||||
builder.WithStatus(status)
|
||||
builder.WithOrder(order)
|
||||
builder.WithDirection(direction)
|
||||
@@ -169,55 +170,52 @@ func (c *Controller) GetEntries(ctx *handler.Context, request *handler.Request,
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch the list of entries"))
|
||||
json.ServerError(w, errors.New("Unable to fetch the list of entries"))
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to count the number of entries"))
|
||||
json.ServerError(w, errors.New("Unable to count the number of entries"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(&entriesResponse{Total: count, Entries: entries})
|
||||
json.OK(w, &entriesResponse{Total: count, Entries: entries})
|
||||
}
|
||||
|
||||
// SetEntryStatus is the API handler to change the status of entries.
|
||||
func (c *Controller) SetEntryStatus(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
|
||||
entryIDs, status, err := decodeEntryStatusPayload(request.Body())
|
||||
func (c *Controller) SetEntryStatus(w http.ResponseWriter, r *http.Request) {
|
||||
entryIDs, status, err := decodeEntryStatusPayload(r.Body)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(errors.New("Invalid JSON payload"))
|
||||
json.BadRequest(w, errors.New("Invalid JSON payload"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := model.ValidateEntryStatus(status); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.SetEntriesStatus(userID, entryIDs, status); err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to change entries status"))
|
||||
if err := c.store.SetEntriesStatus(context.New(r).UserID(), entryIDs, status); err != nil {
|
||||
json.ServerError(w, errors.New("Unable to change entries status"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().NoContent()
|
||||
json.NoContent(w)
|
||||
}
|
||||
|
||||
// ToggleBookmark is the API handler to toggle bookmark status.
|
||||
func (c *Controller) ToggleBookmark(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
func (c *Controller) ToggleBookmark(w http.ResponseWriter, r *http.Request) {
|
||||
entryID, err := request.IntParam(r, "entryID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.ToggleBookmark(userID, entryID); err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to toggle bookmark value"))
|
||||
if err := c.store.ToggleBookmark(context.New(r).UserID(), entryID); err != nil {
|
||||
json.ServerError(w, errors.New("Unable to toggle bookmark value"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().NoContent()
|
||||
json.NoContent(w)
|
||||
}
|
||||
|
||||
+59
-63
@@ -6,44 +6,47 @@ package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/reader/opml"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/json"
|
||||
)
|
||||
|
||||
// CreateFeed is the API handler to create a new feed.
|
||||
func (c *Controller) CreateFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedURL, categoryID, err := decodeFeedCreationPayload(request.Body())
|
||||
func (c *Controller) CreateFeed(w http.ResponseWriter, r *http.Request) {
|
||||
feedURL, categoryID, err := decodeFeedCreationPayload(r.Body)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if feedURL == "" {
|
||||
response.JSON().BadRequest(errors.New("The feed_url is required"))
|
||||
json.BadRequest(w, errors.New("The feed_url is required"))
|
||||
return
|
||||
}
|
||||
|
||||
if categoryID <= 0 {
|
||||
response.JSON().BadRequest(errors.New("The category_id is required"))
|
||||
json.BadRequest(w, errors.New("The category_id is required"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
|
||||
if c.store.FeedURLExists(userID, feedURL) {
|
||||
response.JSON().BadRequest(errors.New("This feed_url already exists"))
|
||||
json.BadRequest(w, errors.New("This feed_url already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
if !c.store.CategoryExists(userID, categoryID) {
|
||||
response.JSON().BadRequest(errors.New("This category_id doesn't exists or doesn't belongs to this user"))
|
||||
json.BadRequest(w, errors.New("This category_id doesn't exists or doesn't belongs to this user"))
|
||||
return
|
||||
}
|
||||
|
||||
feed, err := c.feedHandler.CreateFeed(userID, categoryID, feedURL, false)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to create this feed"))
|
||||
json.ServerError(w, errors.New("Unable to create this feed"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -51,142 +54,135 @@ func (c *Controller) CreateFeed(ctx *handler.Context, request *handler.Request,
|
||||
FeedID int64 `json:"feed_id"`
|
||||
}
|
||||
|
||||
response.JSON().Created(&result{FeedID: feed.ID})
|
||||
json.Created(w, &result{FeedID: feed.ID})
|
||||
}
|
||||
|
||||
// RefreshFeed is the API handler to refresh a feed.
|
||||
func (c *Controller) RefreshFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
func (c *Controller) RefreshFeed(w http.ResponseWriter, r *http.Request) {
|
||||
feedID, err := request.IntParam(r, "feedID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
|
||||
if !c.store.FeedExists(userID, feedID) {
|
||||
response.JSON().NotFound(errors.New("Unable to find this feed"))
|
||||
json.NotFound(w, errors.New("Unable to find this feed"))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.feedHandler.RefreshFeed(userID, feedID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to refresh this feed"))
|
||||
json.ServerError(w, errors.New("Unable to refresh this feed"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().NoContent()
|
||||
json.NoContent(w)
|
||||
}
|
||||
|
||||
// UpdateFeed is the API handler that is used to update a feed.
|
||||
func (c *Controller) UpdateFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
func (c *Controller) UpdateFeed(w http.ResponseWriter, r *http.Request) {
|
||||
feedID, err := request.IntParam(r, "feedID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
newFeed, err := decodeFeedModificationPayload(request.Body())
|
||||
newFeed, err := decodeFeedModificationPayload(r.Body)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
|
||||
if newFeed.Category != nil && newFeed.Category.ID != 0 && !c.store.CategoryExists(userID, newFeed.Category.ID) {
|
||||
response.JSON().BadRequest(errors.New("This category_id doesn't exists or doesn't belongs to this user"))
|
||||
json.BadRequest(w, errors.New("This category_id doesn't exists or doesn't belongs to this user"))
|
||||
return
|
||||
}
|
||||
|
||||
originalFeed, err := c.store.FeedByID(userID, feedID)
|
||||
if err != nil {
|
||||
response.JSON().NotFound(errors.New("Unable to find this feed"))
|
||||
json.NotFound(w, errors.New("Unable to find this feed"))
|
||||
return
|
||||
}
|
||||
|
||||
if originalFeed == nil {
|
||||
response.JSON().NotFound(errors.New("Feed not found"))
|
||||
json.NotFound(w, errors.New("Feed not found"))
|
||||
return
|
||||
}
|
||||
|
||||
originalFeed.Merge(newFeed)
|
||||
if err := c.store.UpdateFeed(originalFeed); err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to update this feed"))
|
||||
json.ServerError(w, errors.New("Unable to update this feed"))
|
||||
return
|
||||
}
|
||||
|
||||
originalFeed, err = c.store.FeedByID(userID, feedID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch this feed"))
|
||||
json.ServerError(w, errors.New("Unable to fetch this feed"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Created(originalFeed)
|
||||
json.Created(w, originalFeed)
|
||||
}
|
||||
|
||||
// GetFeeds is the API handler that get all feeds that belongs to the given user.
|
||||
func (c *Controller) GetFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
feeds, err := c.store.Feeds(ctx.UserID())
|
||||
func (c *Controller) GetFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
feeds, err := c.store.Feeds(context.New(r).UserID())
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch feeds from the database"))
|
||||
json.ServerError(w, errors.New("Unable to fetch feeds from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(feeds)
|
||||
}
|
||||
|
||||
// Export is the API handler that incoves an OPML export.
|
||||
func (c *Controller) Export(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
opmlHandler := opml.NewHandler(c.store)
|
||||
|
||||
opml, err := opmlHandler.Export(ctx.LoggedUser().ID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("unable to export feeds to OPML"))
|
||||
}
|
||||
|
||||
response.XML().Serve(opml)
|
||||
json.OK(w, feeds)
|
||||
}
|
||||
|
||||
// GetFeed is the API handler to get a feed.
|
||||
func (c *Controller) GetFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
func (c *Controller) GetFeed(w http.ResponseWriter, r *http.Request) {
|
||||
feedID, err := request.IntParam(r, "feedID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
feed, err := c.store.FeedByID(userID, feedID)
|
||||
feed, err := c.store.FeedByID(context.New(r).UserID(), feedID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch this feed"))
|
||||
json.ServerError(w, errors.New("Unable to fetch this feed"))
|
||||
return
|
||||
}
|
||||
|
||||
if feed == nil {
|
||||
response.JSON().NotFound(errors.New("Feed not found"))
|
||||
json.NotFound(w, errors.New("Feed not found"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(feed)
|
||||
json.OK(w, feed)
|
||||
}
|
||||
|
||||
// RemoveFeed is the API handler to remove a feed.
|
||||
func (c *Controller) RemoveFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
func (c *Controller) RemoveFeed(w http.ResponseWriter, r *http.Request) {
|
||||
feedID, err := request.IntParam(r, "feedID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
|
||||
if !c.store.FeedExists(userID, feedID) {
|
||||
response.JSON().NotFound(errors.New("Feed not found"))
|
||||
json.NotFound(w, errors.New("Feed not found"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.RemoveFeed(userID, feedID); err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to remove this feed"))
|
||||
json.ServerError(w, errors.New("Unable to remove this feed"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().NoContent()
|
||||
json.NoContent(w)
|
||||
}
|
||||
|
||||
+12
-10
@@ -6,36 +6,38 @@ package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/json"
|
||||
)
|
||||
|
||||
// FeedIcon returns a feed icon.
|
||||
func (c *Controller) FeedIcon(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
func (c *Controller) FeedIcon(w http.ResponseWriter, r *http.Request) {
|
||||
feedID, err := request.IntParam(r, "feedID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.store.HasIcon(feedID) {
|
||||
response.JSON().NotFound(errors.New("This feed doesn't have any icon"))
|
||||
json.NotFound(w, errors.New("This feed doesn't have any icon"))
|
||||
return
|
||||
}
|
||||
|
||||
icon, err := c.store.IconByFeedID(userID, feedID)
|
||||
icon, err := c.store.IconByFeedID(context.New(r).UserID(), feedID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch feed icon"))
|
||||
json.ServerError(w, errors.New("Unable to fetch feed icon"))
|
||||
return
|
||||
}
|
||||
|
||||
if icon == nil {
|
||||
response.JSON().NotFound(errors.New("This feed doesn't have any icon"))
|
||||
json.NotFound(w, errors.New("This feed doesn't have any icon"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(&feedIcon{
|
||||
json.OK(w, &feedIcon{
|
||||
ID: icon.ID,
|
||||
MimeType: icon.MimeType,
|
||||
Data: icon.DataURL(),
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response/json"
|
||||
"github.com/miniflux/miniflux/http/response/xml"
|
||||
"github.com/miniflux/miniflux/reader/opml"
|
||||
)
|
||||
|
||||
// Export is the API handler that export feeds to OPML.
|
||||
func (c *Controller) Export(w http.ResponseWriter, r *http.Request) {
|
||||
opmlHandler := opml.NewHandler(c.store)
|
||||
opml, err := opmlHandler.Export(context.New(r).UserID())
|
||||
if err != nil {
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
xml.OK(w, opml)
|
||||
}
|
||||
|
||||
// Import is the API handler that import an OPML file.
|
||||
func (c *Controller) Import(w http.ResponseWriter, r *http.Request) {
|
||||
opmlHandler := opml.NewHandler(c.store)
|
||||
err := opmlHandler.Import(context.New(r).UserID(), r.Body)
|
||||
defer r.Body.Close()
|
||||
if err != nil {
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.Created(w, map[string]string{"message": "Feeds imported successfully"})
|
||||
}
|
||||
+18
-12
@@ -23,10 +23,11 @@ type entriesResponse struct {
|
||||
Entries model.Entries `json:"entries"`
|
||||
}
|
||||
|
||||
func decodeUserPayload(data io.Reader) (*model.User, error) {
|
||||
func decodeUserPayload(r io.ReadCloser) (*model.User, error) {
|
||||
var user model.User
|
||||
|
||||
decoder := json.NewDecoder(data)
|
||||
decoder := json.NewDecoder(r)
|
||||
defer r.Close()
|
||||
if err := decoder.Decode(&user); err != nil {
|
||||
return nil, fmt.Errorf("Unable to decode user JSON object: %v", err)
|
||||
}
|
||||
@@ -34,13 +35,14 @@ func decodeUserPayload(data io.Reader) (*model.User, error) {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func decodeURLPayload(data io.Reader) (string, error) {
|
||||
func decodeURLPayload(r io.ReadCloser) (string, error) {
|
||||
type payload struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
var p payload
|
||||
decoder := json.NewDecoder(data)
|
||||
decoder := json.NewDecoder(r)
|
||||
defer r.Close()
|
||||
if err := decoder.Decode(&p); err != nil {
|
||||
return "", fmt.Errorf("invalid JSON payload: %v", err)
|
||||
}
|
||||
@@ -48,14 +50,15 @@ func decodeURLPayload(data io.Reader) (string, error) {
|
||||
return p.URL, nil
|
||||
}
|
||||
|
||||
func decodeEntryStatusPayload(data io.Reader) ([]int64, string, error) {
|
||||
func decodeEntryStatusPayload(r io.ReadCloser) ([]int64, string, error) {
|
||||
type payload struct {
|
||||
EntryIDs []int64 `json:"entry_ids"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
var p payload
|
||||
decoder := json.NewDecoder(data)
|
||||
decoder := json.NewDecoder(r)
|
||||
defer r.Close()
|
||||
if err := decoder.Decode(&p); err != nil {
|
||||
return nil, "", fmt.Errorf("invalid JSON payload: %v", err)
|
||||
}
|
||||
@@ -63,14 +66,15 @@ func decodeEntryStatusPayload(data io.Reader) ([]int64, string, error) {
|
||||
return p.EntryIDs, p.Status, nil
|
||||
}
|
||||
|
||||
func decodeFeedCreationPayload(data io.Reader) (string, int64, error) {
|
||||
func decodeFeedCreationPayload(r io.ReadCloser) (string, int64, error) {
|
||||
type payload struct {
|
||||
FeedURL string `json:"feed_url"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
}
|
||||
|
||||
var p payload
|
||||
decoder := json.NewDecoder(data)
|
||||
decoder := json.NewDecoder(r)
|
||||
defer r.Close()
|
||||
if err := decoder.Decode(&p); err != nil {
|
||||
return "", 0, fmt.Errorf("invalid JSON payload: %v", err)
|
||||
}
|
||||
@@ -78,10 +82,11 @@ func decodeFeedCreationPayload(data io.Reader) (string, int64, error) {
|
||||
return p.FeedURL, p.CategoryID, nil
|
||||
}
|
||||
|
||||
func decodeFeedModificationPayload(data io.Reader) (*model.Feed, error) {
|
||||
func decodeFeedModificationPayload(r io.ReadCloser) (*model.Feed, error) {
|
||||
var feed model.Feed
|
||||
|
||||
decoder := json.NewDecoder(data)
|
||||
decoder := json.NewDecoder(r)
|
||||
defer r.Close()
|
||||
if err := decoder.Decode(&feed); err != nil {
|
||||
return nil, fmt.Errorf("Unable to decode feed JSON object: %v", err)
|
||||
}
|
||||
@@ -89,10 +94,11 @@ func decodeFeedModificationPayload(data io.Reader) (*model.Feed, error) {
|
||||
return &feed, nil
|
||||
}
|
||||
|
||||
func decodeCategoryPayload(data io.Reader) (*model.Category, error) {
|
||||
func decodeCategoryPayload(r io.ReadCloser) (*model.Category, error) {
|
||||
var category model.Category
|
||||
|
||||
decoder := json.NewDecoder(data)
|
||||
decoder := json.NewDecoder(r)
|
||||
defer r.Close()
|
||||
if err := decoder.Decode(&category); err != nil {
|
||||
return nil, fmt.Errorf("Unable to decode category JSON object: %v", err)
|
||||
}
|
||||
|
||||
+8
-7
@@ -7,29 +7,30 @@ package api
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/http/response/json"
|
||||
"github.com/miniflux/miniflux/reader/subscription"
|
||||
)
|
||||
|
||||
// GetSubscriptions is the API handler to find subscriptions.
|
||||
func (c *Controller) GetSubscriptions(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
websiteURL, err := decodeURLPayload(request.Body())
|
||||
func (c *Controller) GetSubscriptions(w http.ResponseWriter, r *http.Request) {
|
||||
websiteURL, err := decodeURLPayload(r.Body)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
subscriptions, err := subscription.FindSubscriptions(websiteURL)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to discover subscriptions"))
|
||||
json.ServerError(w, errors.New("Unable to discover subscriptions"))
|
||||
return
|
||||
}
|
||||
|
||||
if subscriptions == nil {
|
||||
response.JSON().NotFound(fmt.Errorf("No subscription found"))
|
||||
json.NotFound(w, fmt.Errorf("No subscription found"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(subscriptions)
|
||||
json.OK(w, subscriptions)
|
||||
}
|
||||
|
||||
+68
-47
@@ -6,182 +6,203 @@ package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/json"
|
||||
)
|
||||
|
||||
// CreateUser is the API handler to create a new user.
|
||||
func (c *Controller) CreateUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
if !ctx.IsAdminUser() {
|
||||
response.JSON().Forbidden()
|
||||
// CurrentUser is the API handler to retrieve the authenticated user.
|
||||
func (c *Controller) CurrentUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := decodeUserPayload(request.Body())
|
||||
json.OK(w, user)
|
||||
}
|
||||
|
||||
// CreateUser is the API handler to create a new user.
|
||||
func (c *Controller) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
if !ctx.IsAdminUser() {
|
||||
json.Forbidden(w)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := decodeUserPayload(r.Body)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.ValidateUserCreation(); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if c.store.UserExists(user.Username) {
|
||||
response.JSON().BadRequest(errors.New("This user already exists"))
|
||||
json.BadRequest(w, errors.New("This user already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.CreateUser(user)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to create this user"))
|
||||
json.ServerError(w, errors.New("Unable to create this user"))
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = ""
|
||||
response.JSON().Created(user)
|
||||
json.Created(w, user)
|
||||
}
|
||||
|
||||
// UpdateUser is the API handler to update the given user.
|
||||
func (c *Controller) UpdateUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
if !ctx.IsAdminUser() {
|
||||
response.JSON().Forbidden()
|
||||
json.Forbidden(w)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := request.IntegerParam("userID")
|
||||
userID, err := request.IntParam(r, "userID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := decodeUserPayload(request.Body())
|
||||
user, err := decodeUserPayload(r.Body)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.ValidateUserModification(); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
originalUser, err := c.store.UserByID(userID)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(errors.New("Unable to fetch this user from the database"))
|
||||
json.BadRequest(w, errors.New("Unable to fetch this user from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
if originalUser == nil {
|
||||
response.JSON().NotFound(errors.New("User not found"))
|
||||
json.NotFound(w, errors.New("User not found"))
|
||||
return
|
||||
}
|
||||
|
||||
originalUser.Merge(user)
|
||||
if err = c.store.UpdateUser(originalUser); err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to update this user"))
|
||||
json.ServerError(w, errors.New("Unable to update this user"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Created(originalUser)
|
||||
json.Created(w, originalUser)
|
||||
}
|
||||
|
||||
// Users is the API handler to get the list of users.
|
||||
func (c *Controller) Users(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) Users(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
if !ctx.IsAdminUser() {
|
||||
response.JSON().Forbidden()
|
||||
json.Forbidden(w)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := c.store.Users()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch the list of users"))
|
||||
json.ServerError(w, errors.New("Unable to fetch the list of users"))
|
||||
return
|
||||
}
|
||||
|
||||
users.UseTimezone(ctx.UserTimezone())
|
||||
response.JSON().Standard(users)
|
||||
json.OK(w, users)
|
||||
}
|
||||
|
||||
// UserByID is the API handler to fetch the given user by the ID.
|
||||
func (c *Controller) UserByID(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) UserByID(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
if !ctx.IsAdminUser() {
|
||||
response.JSON().Forbidden()
|
||||
json.Forbidden(w)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := request.IntegerParam("userID")
|
||||
userID, err := request.IntParam(r, "userID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := c.store.UserByID(userID)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(errors.New("Unable to fetch this user from the database"))
|
||||
json.BadRequest(w, errors.New("Unable to fetch this user from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
response.JSON().NotFound(errors.New("User not found"))
|
||||
json.NotFound(w, errors.New("User not found"))
|
||||
return
|
||||
}
|
||||
|
||||
user.UseTimezone(ctx.UserTimezone())
|
||||
response.JSON().Standard(user)
|
||||
json.OK(w, user)
|
||||
}
|
||||
|
||||
// UserByUsername is the API handler to fetch the given user by the username.
|
||||
func (c *Controller) UserByUsername(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) UserByUsername(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
if !ctx.IsAdminUser() {
|
||||
response.JSON().Forbidden()
|
||||
json.Forbidden(w)
|
||||
return
|
||||
}
|
||||
|
||||
username := request.StringParam("username", "")
|
||||
username := request.Param(r, "username", "")
|
||||
user, err := c.store.UserByUsername(username)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(errors.New("Unable to fetch this user from the database"))
|
||||
json.BadRequest(w, errors.New("Unable to fetch this user from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
response.JSON().NotFound(errors.New("User not found"))
|
||||
json.NotFound(w, errors.New("User not found"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(user)
|
||||
json.OK(w, user)
|
||||
}
|
||||
|
||||
// RemoveUser is the API handler to remove an existing user.
|
||||
func (c *Controller) RemoveUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
if !ctx.IsAdminUser() {
|
||||
response.JSON().Forbidden()
|
||||
json.Forbidden(w)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := request.IntegerParam("userID")
|
||||
userID, err := request.IntParam(r, "userID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := c.store.UserByID(userID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch this user from the database"))
|
||||
json.ServerError(w, errors.New("Unable to fetch this user from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
response.JSON().NotFound(errors.New("User not found"))
|
||||
json.NotFound(w, errors.New("User not found"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.RemoveUser(user.ID); err != nil {
|
||||
response.JSON().BadRequest(errors.New("Unable to remove this user from the database"))
|
||||
json.BadRequest(w, errors.New("Unable to remove this user from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().NoContent()
|
||||
json.NoContent(w)
|
||||
}
|
||||
|
||||
+20
-15
@@ -13,18 +13,18 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBaseURL = "http://localhost"
|
||||
defaultDatabaseURL = "postgres://postgres:postgres@localhost/miniflux2?sslmode=disable"
|
||||
defaultWorkerPoolSize = 5
|
||||
defaultPollingFrequency = 60
|
||||
defaultBatchSize = 10
|
||||
defaultDatabaseMaxConns = 20
|
||||
defaultListenAddr = "127.0.0.1:8080"
|
||||
defaultCertFile = ""
|
||||
defaultKeyFile = ""
|
||||
defaultCertDomain = ""
|
||||
defaultCertCache = "/tmp/cert_cache"
|
||||
defaultSessionCleanupFrequency = 24
|
||||
defaultBaseURL = "http://localhost"
|
||||
defaultDatabaseURL = "postgres://postgres:postgres@localhost/miniflux2?sslmode=disable"
|
||||
defaultWorkerPoolSize = 5
|
||||
defaultPollingFrequency = 60
|
||||
defaultBatchSize = 10
|
||||
defaultDatabaseMaxConns = 20
|
||||
defaultListenAddr = "127.0.0.1:8080"
|
||||
defaultCertFile = ""
|
||||
defaultKeyFile = ""
|
||||
defaultCertDomain = ""
|
||||
defaultCertCache = "/tmp/cert_cache"
|
||||
defaultCleanupFrequency = 24
|
||||
)
|
||||
|
||||
// Config manages configuration parameters.
|
||||
@@ -137,9 +137,9 @@ func (c *Config) CertCache() string {
|
||||
return c.get("CERT_CACHE", defaultCertCache)
|
||||
}
|
||||
|
||||
// SessionCleanupFrequency returns the interval for session cleanup.
|
||||
func (c *Config) SessionCleanupFrequency() int {
|
||||
return c.getInt("SESSION_CLEANUP_FREQUENCY", defaultSessionCleanupFrequency)
|
||||
// CleanupFrequency returns the interval for cleanup jobs.
|
||||
func (c *Config) CleanupFrequency() int {
|
||||
return c.getInt("CLEANUP_FREQUENCY", defaultCleanupFrequency)
|
||||
}
|
||||
|
||||
// WorkerPoolSize returns the number of background worker.
|
||||
@@ -197,6 +197,11 @@ func (c *Config) CreateAdmin() bool {
|
||||
return c.get("CREATE_ADMIN", "") != ""
|
||||
}
|
||||
|
||||
// PocketConsumerKey returns the Pocket Consumer Key if defined as environment variable.
|
||||
func (c *Config) PocketConsumerKey(defaultValue string) string {
|
||||
return c.get("POCKET_CONSUMER_KEY", defaultValue)
|
||||
}
|
||||
|
||||
// NewConfig returns a new Config.
|
||||
func NewConfig() *Config {
|
||||
return &Config{IsHTTPS: os.Getenv("HTTPS") != ""}
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ func Run(cfg *config.Config, store *storage.Storage) {
|
||||
cfg.BatchSize(),
|
||||
)
|
||||
|
||||
scheduler.NewSessionScheduler(store, cfg.SessionCleanupFrequency())
|
||||
scheduler.NewCleanupScheduler(store, cfg.CleanupFrequency())
|
||||
|
||||
<-stop
|
||||
logger.Info("Shutting down the server...")
|
||||
|
||||
+108
-128
@@ -10,11 +10,9 @@ import (
|
||||
"github.com/miniflux/miniflux/api"
|
||||
"github.com/miniflux/miniflux/config"
|
||||
"github.com/miniflux/miniflux/fever"
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/http/middleware"
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
"github.com/miniflux/miniflux/middleware"
|
||||
"github.com/miniflux/miniflux/reader/feed"
|
||||
"github.com/miniflux/miniflux/reader/opml"
|
||||
"github.com/miniflux/miniflux/scheduler"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
"github.com/miniflux/miniflux/template"
|
||||
@@ -26,137 +24,18 @@ import (
|
||||
func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handler, pool *scheduler.WorkerPool, translator *locale.Translator) *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
templateEngine := template.NewEngine(cfg, router, translator)
|
||||
|
||||
apiController := api.NewController(store, feedHandler)
|
||||
feverController := fever.NewController(store)
|
||||
uiController := ui.NewController(cfg, store, pool, feedHandler, opml.NewHandler(store))
|
||||
|
||||
apiHandler := handler.NewHandler(cfg, store, router, templateEngine, translator, middleware.NewChain(
|
||||
middleware.NewBasicAuthMiddleware(store).Handler,
|
||||
))
|
||||
|
||||
feverHandler := handler.NewHandler(cfg, store, router, templateEngine, translator, middleware.NewChain(
|
||||
middleware.NewFeverMiddleware(store).Handler,
|
||||
))
|
||||
|
||||
uiHandler := handler.NewHandler(cfg, store, router, templateEngine, translator, middleware.NewChain(
|
||||
middleware.NewUserSessionMiddleware(store, router).Handler,
|
||||
middleware.NewSessionMiddleware(cfg, store).Handler,
|
||||
))
|
||||
feverController := fever.NewController(cfg, store)
|
||||
uiController := ui.NewController(cfg, store, pool, feedHandler, templateEngine, translator, router)
|
||||
middleware := middleware.New(cfg, store, router)
|
||||
|
||||
if cfg.BasePath() != "" {
|
||||
router = router.PathPrefix(cfg.BasePath()).Subrouter()
|
||||
}
|
||||
|
||||
router.Handle("/fever/", feverHandler.Use(feverController.Handler)).Name("feverEndpoint")
|
||||
|
||||
router.Handle("/v1/users", apiHandler.Use(apiController.CreateUser)).Methods("POST")
|
||||
router.Handle("/v1/users", apiHandler.Use(apiController.Users)).Methods("GET")
|
||||
router.Handle("/v1/users/{userID:[0-9]+}", apiHandler.Use(apiController.UserByID)).Methods("GET")
|
||||
router.Handle("/v1/users/{userID:[0-9]+}", apiHandler.Use(apiController.UpdateUser)).Methods("PUT")
|
||||
router.Handle("/v1/users/{userID:[0-9]+}", apiHandler.Use(apiController.RemoveUser)).Methods("DELETE")
|
||||
router.Handle("/v1/users/{username}", apiHandler.Use(apiController.UserByUsername)).Methods("GET")
|
||||
|
||||
router.Handle("/v1/categories", apiHandler.Use(apiController.CreateCategory)).Methods("POST")
|
||||
router.Handle("/v1/categories", apiHandler.Use(apiController.GetCategories)).Methods("GET")
|
||||
router.Handle("/v1/categories/{categoryID}", apiHandler.Use(apiController.UpdateCategory)).Methods("PUT")
|
||||
router.Handle("/v1/categories/{categoryID}", apiHandler.Use(apiController.RemoveCategory)).Methods("DELETE")
|
||||
|
||||
router.Handle("/v1/discover", apiHandler.Use(apiController.GetSubscriptions)).Methods("POST")
|
||||
|
||||
router.Handle("/v1/feeds", apiHandler.Use(apiController.CreateFeed)).Methods("POST")
|
||||
router.Handle("/v1/feeds", apiHandler.Use(apiController.GetFeeds)).Methods("Get")
|
||||
router.Handle("/v1/feeds/{feedID}/refresh", apiHandler.Use(apiController.RefreshFeed)).Methods("PUT")
|
||||
router.Handle("/v1/feeds/{feedID}", apiHandler.Use(apiController.GetFeed)).Methods("GET")
|
||||
router.Handle("/v1/feeds/{feedID}", apiHandler.Use(apiController.UpdateFeed)).Methods("PUT")
|
||||
router.Handle("/v1/feeds/{feedID}", apiHandler.Use(apiController.RemoveFeed)).Methods("DELETE")
|
||||
router.Handle("/v1/feeds/{feedID}/icon", apiHandler.Use(apiController.FeedIcon)).Methods("GET")
|
||||
router.Handle("/v1/export", apiHandler.Use(apiController.Export)).Methods("GET")
|
||||
|
||||
router.Handle("/v1/feeds/{feedID}/entries", apiHandler.Use(apiController.GetFeedEntries)).Methods("GET")
|
||||
router.Handle("/v1/feeds/{feedID}/entries/{entryID}", apiHandler.Use(apiController.GetFeedEntry)).Methods("GET")
|
||||
router.Handle("/v1/entries", apiHandler.Use(apiController.GetEntries)).Methods("GET")
|
||||
router.Handle("/v1/entries", apiHandler.Use(apiController.SetEntryStatus)).Methods("PUT")
|
||||
router.Handle("/v1/entries/{entryID}", apiHandler.Use(apiController.GetEntry)).Methods("GET")
|
||||
router.Handle("/v1/entries/{entryID}/bookmark", apiHandler.Use(apiController.ToggleBookmark)).Methods("PUT")
|
||||
|
||||
router.Handle("/stylesheets/{name}.css", uiHandler.Use(uiController.Stylesheet)).Name("stylesheet").Methods("GET")
|
||||
router.Handle("/js", uiHandler.Use(uiController.Javascript)).Name("javascript").Methods("GET")
|
||||
router.Handle("/favicon.ico", uiHandler.Use(uiController.Favicon)).Name("favicon").Methods("GET")
|
||||
router.Handle("/icon/{filename}", uiHandler.Use(uiController.AppIcon)).Name("appIcon").Methods("GET")
|
||||
router.Handle("/manifest.json", uiHandler.Use(uiController.WebManifest)).Name("webManifest").Methods("GET")
|
||||
|
||||
router.Handle("/subscribe", uiHandler.Use(uiController.AddSubscription)).Name("addSubscription").Methods("GET")
|
||||
router.Handle("/subscribe", uiHandler.Use(uiController.SubmitSubscription)).Name("submitSubscription").Methods("POST")
|
||||
router.Handle("/subscriptions", uiHandler.Use(uiController.ChooseSubscription)).Name("chooseSubscription").Methods("POST")
|
||||
|
||||
router.Handle("/mark-all-as-read", uiHandler.Use(uiController.MarkAllAsRead)).Name("markAllAsRead").Methods("GET")
|
||||
|
||||
router.Handle("/unread", uiHandler.Use(uiController.ShowUnreadPage)).Name("unread").Methods("GET")
|
||||
router.Handle("/history", uiHandler.Use(uiController.ShowHistoryPage)).Name("history").Methods("GET")
|
||||
router.Handle("/starred", uiHandler.Use(uiController.ShowStarredPage)).Name("starred").Methods("GET")
|
||||
|
||||
router.Handle("/feed/{feedID}/refresh", uiHandler.Use(uiController.RefreshFeed)).Name("refreshFeed").Methods("GET")
|
||||
router.Handle("/feed/{feedID}/edit", uiHandler.Use(uiController.EditFeed)).Name("editFeed").Methods("GET")
|
||||
router.Handle("/feed/{feedID}/remove", uiHandler.Use(uiController.RemoveFeed)).Name("removeFeed").Methods("POST")
|
||||
router.Handle("/feed/{feedID}/update", uiHandler.Use(uiController.UpdateFeed)).Name("updateFeed").Methods("POST")
|
||||
router.Handle("/feed/{feedID}/entries", uiHandler.Use(uiController.ShowFeedEntries)).Name("feedEntries").Methods("GET")
|
||||
router.Handle("/feeds", uiHandler.Use(uiController.ShowFeedsPage)).Name("feeds").Methods("GET")
|
||||
router.Handle("/feeds/refresh", uiHandler.Use(uiController.RefreshAllFeeds)).Name("refreshAllFeeds").Methods("GET")
|
||||
|
||||
router.Handle("/unread/entry/{entryID}", uiHandler.Use(uiController.ShowUnreadEntry)).Name("unreadEntry").Methods("GET")
|
||||
router.Handle("/history/entry/{entryID}", uiHandler.Use(uiController.ShowReadEntry)).Name("readEntry").Methods("GET")
|
||||
router.Handle("/history/flush", uiHandler.Use(uiController.FlushHistory)).Name("flushHistory").Methods("GET")
|
||||
router.Handle("/feed/{feedID}/entry/{entryID}", uiHandler.Use(uiController.ShowFeedEntry)).Name("feedEntry").Methods("GET")
|
||||
router.Handle("/category/{categoryID}/entry/{entryID}", uiHandler.Use(uiController.ShowCategoryEntry)).Name("categoryEntry").Methods("GET")
|
||||
router.Handle("/starred/entry/{entryID}", uiHandler.Use(uiController.ShowStarredEntry)).Name("starredEntry").Methods("GET")
|
||||
|
||||
router.Handle("/entry/status", uiHandler.Use(uiController.UpdateEntriesStatus)).Name("updateEntriesStatus").Methods("POST")
|
||||
router.Handle("/entry/save/{entryID}", uiHandler.Use(uiController.SaveEntry)).Name("saveEntry").Methods("POST")
|
||||
router.Handle("/entry/download/{entryID}", uiHandler.Use(uiController.FetchContent)).Name("fetchContent").Methods("POST")
|
||||
router.Handle("/entry/bookmark/{entryID}", uiHandler.Use(uiController.ToggleBookmark)).Name("toggleBookmark").Methods("POST")
|
||||
|
||||
router.Handle("/categories", uiHandler.Use(uiController.ShowCategories)).Name("categories").Methods("GET")
|
||||
router.Handle("/category/create", uiHandler.Use(uiController.CreateCategory)).Name("createCategory").Methods("GET")
|
||||
router.Handle("/category/save", uiHandler.Use(uiController.SaveCategory)).Name("saveCategory").Methods("POST")
|
||||
router.Handle("/category/{categoryID}/entries", uiHandler.Use(uiController.ShowCategoryEntries)).Name("categoryEntries").Methods("GET")
|
||||
router.Handle("/category/{categoryID}/edit", uiHandler.Use(uiController.EditCategory)).Name("editCategory").Methods("GET")
|
||||
router.Handle("/category/{categoryID}/update", uiHandler.Use(uiController.UpdateCategory)).Name("updateCategory").Methods("POST")
|
||||
router.Handle("/category/{categoryID}/remove", uiHandler.Use(uiController.RemoveCategory)).Name("removeCategory").Methods("POST")
|
||||
|
||||
router.Handle("/feed/icon/{iconID}", uiHandler.Use(uiController.ShowIcon)).Name("icon").Methods("GET")
|
||||
router.Handle("/proxy/{encodedURL}", uiHandler.Use(uiController.ImageProxy)).Name("proxy").Methods("GET")
|
||||
|
||||
router.Handle("/users", uiHandler.Use(uiController.ShowUsers)).Name("users").Methods("GET")
|
||||
router.Handle("/user/create", uiHandler.Use(uiController.CreateUser)).Name("createUser").Methods("GET")
|
||||
router.Handle("/user/save", uiHandler.Use(uiController.SaveUser)).Name("saveUser").Methods("POST")
|
||||
router.Handle("/users/{userID}/edit", uiHandler.Use(uiController.EditUser)).Name("editUser").Methods("GET")
|
||||
router.Handle("/users/{userID}/update", uiHandler.Use(uiController.UpdateUser)).Name("updateUser").Methods("POST")
|
||||
router.Handle("/users/{userID}/remove", uiHandler.Use(uiController.RemoveUser)).Name("removeUser").Methods("POST")
|
||||
|
||||
router.Handle("/about", uiHandler.Use(uiController.AboutPage)).Name("about").Methods("GET")
|
||||
|
||||
router.Handle("/settings", uiHandler.Use(uiController.ShowSettings)).Name("settings").Methods("GET")
|
||||
router.Handle("/settings", uiHandler.Use(uiController.UpdateSettings)).Name("updateSettings").Methods("POST")
|
||||
|
||||
router.Handle("/bookmarklet", uiHandler.Use(uiController.Bookmarklet)).Name("bookmarklet").Methods("GET")
|
||||
router.Handle("/integrations", uiHandler.Use(uiController.ShowIntegrations)).Name("integrations").Methods("GET")
|
||||
router.Handle("/integration", uiHandler.Use(uiController.UpdateIntegration)).Name("updateIntegration").Methods("POST")
|
||||
|
||||
router.Handle("/sessions", uiHandler.Use(uiController.ShowSessions)).Name("sessions").Methods("GET")
|
||||
router.Handle("/sessions/{sessionID}/remove", uiHandler.Use(uiController.RemoveSession)).Name("removeSession").Methods("POST")
|
||||
|
||||
router.Handle("/export", uiHandler.Use(uiController.Export)).Name("export").Methods("GET")
|
||||
router.Handle("/import", uiHandler.Use(uiController.Import)).Name("import").Methods("GET")
|
||||
router.Handle("/upload", uiHandler.Use(uiController.UploadOPML)).Name("uploadOPML").Methods("POST")
|
||||
|
||||
router.Handle("/oauth2/{provider}/unlink", uiHandler.Use(uiController.OAuth2Unlink)).Name("oauth2Unlink").Methods("GET")
|
||||
router.Handle("/oauth2/{provider}/redirect", uiHandler.Use(uiController.OAuth2Redirect)).Name("oauth2Redirect").Methods("GET")
|
||||
router.Handle("/oauth2/{provider}/callback", uiHandler.Use(uiController.OAuth2Callback)).Name("oauth2Callback").Methods("GET")
|
||||
|
||||
router.Handle("/login", uiHandler.Use(uiController.CheckLogin)).Name("checkLogin").Methods("POST")
|
||||
router.Handle("/logout", uiHandler.Use(uiController.Logout)).Name("logout").Methods("GET")
|
||||
router.Handle("/", uiHandler.Use(uiController.ShowLoginPage)).Name("login").Methods("GET")
|
||||
router.Use(middleware.HeaderConfig)
|
||||
router.Use(middleware.Logging)
|
||||
router.Use(middleware.CommonHeaders)
|
||||
|
||||
router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("OK"))
|
||||
@@ -167,5 +46,106 @@ func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handle
|
||||
w.Write([]byte("User-agent: *\nDisallow: /"))
|
||||
})
|
||||
|
||||
feverRouter := router.PathPrefix("/fever").Subrouter()
|
||||
feverRouter.Use(middleware.FeverAuth)
|
||||
feverRouter.HandleFunc("/", feverController.Handler).Name("feverEndpoint")
|
||||
|
||||
apiRouter := router.PathPrefix("/v1").Subrouter()
|
||||
apiRouter.Use(middleware.BasicAuth)
|
||||
apiRouter.HandleFunc("/users", apiController.CreateUser).Methods("POST")
|
||||
apiRouter.HandleFunc("/users", apiController.Users).Methods("GET")
|
||||
apiRouter.HandleFunc("/users/{userID:[0-9]+}", apiController.UserByID).Methods("GET")
|
||||
apiRouter.HandleFunc("/users/{userID:[0-9]+}", apiController.UpdateUser).Methods("PUT")
|
||||
apiRouter.HandleFunc("/users/{userID:[0-9]+}", apiController.RemoveUser).Methods("DELETE")
|
||||
apiRouter.HandleFunc("/users/{username}", apiController.UserByUsername).Methods("GET")
|
||||
apiRouter.HandleFunc("/me", apiController.CurrentUser).Methods("GET")
|
||||
apiRouter.HandleFunc("/categories", apiController.CreateCategory).Methods("POST")
|
||||
apiRouter.HandleFunc("/categories", apiController.GetCategories).Methods("GET")
|
||||
apiRouter.HandleFunc("/categories/{categoryID}", apiController.UpdateCategory).Methods("PUT")
|
||||
apiRouter.HandleFunc("/categories/{categoryID}", apiController.RemoveCategory).Methods("DELETE")
|
||||
apiRouter.HandleFunc("/discover", apiController.GetSubscriptions).Methods("POST")
|
||||
apiRouter.HandleFunc("/feeds", apiController.CreateFeed).Methods("POST")
|
||||
apiRouter.HandleFunc("/feeds", apiController.GetFeeds).Methods("GET")
|
||||
apiRouter.HandleFunc("/feeds/{feedID}/refresh", apiController.RefreshFeed).Methods("PUT")
|
||||
apiRouter.HandleFunc("/feeds/{feedID}", apiController.GetFeed).Methods("GET")
|
||||
apiRouter.HandleFunc("/feeds/{feedID}", apiController.UpdateFeed).Methods("PUT")
|
||||
apiRouter.HandleFunc("/feeds/{feedID}", apiController.RemoveFeed).Methods("DELETE")
|
||||
apiRouter.HandleFunc("/feeds/{feedID}/icon", apiController.FeedIcon).Methods("GET")
|
||||
apiRouter.HandleFunc("/export", apiController.Export).Methods("GET")
|
||||
apiRouter.HandleFunc("/import", apiController.Import).Methods("POST")
|
||||
apiRouter.HandleFunc("/feeds/{feedID}/entries", apiController.GetFeedEntries).Methods("GET")
|
||||
apiRouter.HandleFunc("/feeds/{feedID}/entries/{entryID}", apiController.GetFeedEntry).Methods("GET")
|
||||
apiRouter.HandleFunc("/entries", apiController.GetEntries).Methods("GET")
|
||||
apiRouter.HandleFunc("/entries", apiController.SetEntryStatus).Methods("PUT")
|
||||
apiRouter.HandleFunc("/entries/{entryID}", apiController.GetEntry).Methods("GET")
|
||||
apiRouter.HandleFunc("/entries/{entryID}/bookmark", apiController.ToggleBookmark).Methods("PUT")
|
||||
|
||||
uiRouter := router.NewRoute().Subrouter()
|
||||
uiRouter.Use(middleware.AppSession)
|
||||
uiRouter.Use(middleware.UserSession)
|
||||
uiRouter.HandleFunc("/stylesheets/{name}.css", uiController.Stylesheet).Name("stylesheet").Methods("GET")
|
||||
uiRouter.HandleFunc("/js", uiController.Javascript).Name("javascript").Methods("GET")
|
||||
uiRouter.HandleFunc("/favicon.ico", uiController.Favicon).Name("favicon").Methods("GET")
|
||||
uiRouter.HandleFunc("/icon/{filename}", uiController.AppIcon).Name("appIcon").Methods("GET")
|
||||
uiRouter.HandleFunc("/manifest.json", uiController.WebManifest).Name("webManifest").Methods("GET")
|
||||
uiRouter.HandleFunc("/subscribe", uiController.AddSubscription).Name("addSubscription").Methods("GET")
|
||||
uiRouter.HandleFunc("/subscribe", uiController.SubmitSubscription).Name("submitSubscription").Methods("POST")
|
||||
uiRouter.HandleFunc("/subscriptions", uiController.ChooseSubscription).Name("chooseSubscription").Methods("POST")
|
||||
uiRouter.HandleFunc("/mark-all-as-read", uiController.MarkAllAsRead).Name("markAllAsRead").Methods("GET")
|
||||
uiRouter.HandleFunc("/unread", uiController.ShowUnreadPage).Name("unread").Methods("GET")
|
||||
uiRouter.HandleFunc("/history", uiController.ShowHistoryPage).Name("history").Methods("GET")
|
||||
uiRouter.HandleFunc("/starred", uiController.ShowStarredPage).Name("starred").Methods("GET")
|
||||
uiRouter.HandleFunc("/feed/{feedID}/refresh", uiController.RefreshFeed).Name("refreshFeed").Methods("GET")
|
||||
uiRouter.HandleFunc("/feed/{feedID}/edit", uiController.EditFeed).Name("editFeed").Methods("GET")
|
||||
uiRouter.HandleFunc("/feed/{feedID}/remove", uiController.RemoveFeed).Name("removeFeed").Methods("POST")
|
||||
uiRouter.HandleFunc("/feed/{feedID}/update", uiController.UpdateFeed).Name("updateFeed").Methods("POST")
|
||||
uiRouter.HandleFunc("/feed/{feedID}/entries", uiController.ShowFeedEntries).Name("feedEntries").Methods("GET")
|
||||
uiRouter.HandleFunc("/feeds", uiController.ShowFeedsPage).Name("feeds").Methods("GET")
|
||||
uiRouter.HandleFunc("/feeds/refresh", uiController.RefreshAllFeeds).Name("refreshAllFeeds").Methods("GET")
|
||||
uiRouter.HandleFunc("/unread/entry/{entryID}", uiController.ShowUnreadEntry).Name("unreadEntry").Methods("GET")
|
||||
uiRouter.HandleFunc("/history/entry/{entryID}", uiController.ShowReadEntry).Name("readEntry").Methods("GET")
|
||||
uiRouter.HandleFunc("/history/flush", uiController.FlushHistory).Name("flushHistory").Methods("GET")
|
||||
uiRouter.HandleFunc("/feed/{feedID}/entry/{entryID}", uiController.ShowFeedEntry).Name("feedEntry").Methods("GET")
|
||||
uiRouter.HandleFunc("/category/{categoryID}/entry/{entryID}", uiController.ShowCategoryEntry).Name("categoryEntry").Methods("GET")
|
||||
uiRouter.HandleFunc("/starred/entry/{entryID}", uiController.ShowStarredEntry).Name("starredEntry").Methods("GET")
|
||||
uiRouter.HandleFunc("/entry/status", uiController.UpdateEntriesStatus).Name("updateEntriesStatus").Methods("POST")
|
||||
uiRouter.HandleFunc("/entry/save/{entryID}", uiController.SaveEntry).Name("saveEntry").Methods("POST")
|
||||
uiRouter.HandleFunc("/entry/download/{entryID}", uiController.FetchContent).Name("fetchContent").Methods("POST")
|
||||
uiRouter.HandleFunc("/entry/bookmark/{entryID}", uiController.ToggleBookmark).Name("toggleBookmark").Methods("POST")
|
||||
uiRouter.HandleFunc("/categories", uiController.CategoryList).Name("categories").Methods("GET")
|
||||
uiRouter.HandleFunc("/category/create", uiController.CreateCategory).Name("createCategory").Methods("GET")
|
||||
uiRouter.HandleFunc("/category/save", uiController.SaveCategory).Name("saveCategory").Methods("POST")
|
||||
uiRouter.HandleFunc("/category/{categoryID}/entries", uiController.CategoryEntries).Name("categoryEntries").Methods("GET")
|
||||
uiRouter.HandleFunc("/category/{categoryID}/edit", uiController.EditCategory).Name("editCategory").Methods("GET")
|
||||
uiRouter.HandleFunc("/category/{categoryID}/update", uiController.UpdateCategory).Name("updateCategory").Methods("POST")
|
||||
uiRouter.HandleFunc("/category/{categoryID}/remove", uiController.RemoveCategory).Name("removeCategory").Methods("POST")
|
||||
uiRouter.HandleFunc("/feed/icon/{iconID}", uiController.ShowIcon).Name("icon").Methods("GET")
|
||||
uiRouter.HandleFunc("/proxy/{encodedURL}", uiController.ImageProxy).Name("proxy").Methods("GET")
|
||||
uiRouter.HandleFunc("/users", uiController.ShowUsers).Name("users").Methods("GET")
|
||||
uiRouter.HandleFunc("/user/create", uiController.CreateUser).Name("createUser").Methods("GET")
|
||||
uiRouter.HandleFunc("/user/save", uiController.SaveUser).Name("saveUser").Methods("POST")
|
||||
uiRouter.HandleFunc("/users/{userID}/edit", uiController.EditUser).Name("editUser").Methods("GET")
|
||||
uiRouter.HandleFunc("/users/{userID}/update", uiController.UpdateUser).Name("updateUser").Methods("POST")
|
||||
uiRouter.HandleFunc("/users/{userID}/remove", uiController.RemoveUser).Name("removeUser").Methods("POST")
|
||||
uiRouter.HandleFunc("/about", uiController.About).Name("about").Methods("GET")
|
||||
uiRouter.HandleFunc("/settings", uiController.ShowSettings).Name("settings").Methods("GET")
|
||||
uiRouter.HandleFunc("/settings", uiController.UpdateSettings).Name("updateSettings").Methods("POST")
|
||||
uiRouter.HandleFunc("/bookmarklet", uiController.Bookmarklet).Name("bookmarklet").Methods("GET")
|
||||
uiRouter.HandleFunc("/integrations", uiController.ShowIntegrations).Name("integrations").Methods("GET")
|
||||
uiRouter.HandleFunc("/integration", uiController.UpdateIntegration).Name("updateIntegration").Methods("POST")
|
||||
uiRouter.HandleFunc("/integration/pocket/authorize", uiController.PocketAuthorize).Name("pocketAuthorize").Methods("GET")
|
||||
uiRouter.HandleFunc("/integration/pocket/callback", uiController.PocketCallback).Name("pocketCallback").Methods("GET")
|
||||
uiRouter.HandleFunc("/sessions", uiController.ShowSessions).Name("sessions").Methods("GET")
|
||||
uiRouter.HandleFunc("/sessions/{sessionID}/remove", uiController.RemoveSession).Name("removeSession").Methods("POST")
|
||||
uiRouter.HandleFunc("/export", uiController.Export).Name("export").Methods("GET")
|
||||
uiRouter.HandleFunc("/import", uiController.Import).Name("import").Methods("GET")
|
||||
uiRouter.HandleFunc("/upload", uiController.UploadOPML).Name("uploadOPML").Methods("POST")
|
||||
uiRouter.HandleFunc("/oauth2/{provider}/unlink", uiController.OAuth2Unlink).Name("oauth2Unlink").Methods("GET")
|
||||
uiRouter.HandleFunc("/oauth2/{provider}/redirect", uiController.OAuth2Redirect).Name("oauth2Redirect").Methods("GET")
|
||||
uiRouter.HandleFunc("/oauth2/{provider}/callback", uiController.OAuth2Callback).Name("oauth2Callback").Methods("GET")
|
||||
uiRouter.HandleFunc("/login", uiController.CheckLogin).Name("checkLogin").Methods("POST")
|
||||
uiRouter.HandleFunc("/logout", uiController.Logout).Name("logout").Methods("GET")
|
||||
uiRouter.HandleFunc("/", uiController.ShowLoginPage).Name("login").Methods("GET")
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
+84
-66
@@ -5,11 +5,15 @@
|
||||
package fever
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/config"
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/json"
|
||||
"github.com/miniflux/miniflux/integration"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
@@ -125,32 +129,33 @@ type favicon struct {
|
||||
|
||||
// Controller implements the Fever API.
|
||||
type Controller struct {
|
||||
cfg *config.Config
|
||||
store *storage.Storage
|
||||
}
|
||||
|
||||
// Handler handles Fever API calls
|
||||
func (c *Controller) Handler(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case request.HasQueryParam("groups"):
|
||||
c.handleGroups(ctx, request, response)
|
||||
case request.HasQueryParam("feeds"):
|
||||
c.handleFeeds(ctx, request, response)
|
||||
case request.HasQueryParam("favicons"):
|
||||
c.handleFavicons(ctx, request, response)
|
||||
case request.HasQueryParam("unread_item_ids"):
|
||||
c.handleUnreadItems(ctx, request, response)
|
||||
case request.HasQueryParam("saved_item_ids"):
|
||||
c.handleSavedItems(ctx, request, response)
|
||||
case request.HasQueryParam("items"):
|
||||
c.handleItems(ctx, request, response)
|
||||
case request.FormValue("mark") == "item":
|
||||
c.handleWriteItems(ctx, request, response)
|
||||
case request.FormValue("mark") == "feed":
|
||||
c.handleWriteFeeds(ctx, request, response)
|
||||
case request.FormValue("mark") == "group":
|
||||
c.handleWriteGroups(ctx, request, response)
|
||||
case request.HasQueryParam(r, "groups"):
|
||||
c.handleGroups(w, r)
|
||||
case request.HasQueryParam(r, "feeds"):
|
||||
c.handleFeeds(w, r)
|
||||
case request.HasQueryParam(r, "favicons"):
|
||||
c.handleFavicons(w, r)
|
||||
case request.HasQueryParam(r, "unread_item_ids"):
|
||||
c.handleUnreadItems(w, r)
|
||||
case request.HasQueryParam(r, "saved_item_ids"):
|
||||
c.handleSavedItems(w, r)
|
||||
case request.HasQueryParam(r, "items"):
|
||||
c.handleItems(w, r)
|
||||
case r.FormValue("mark") == "item":
|
||||
c.handleWriteItems(w, r)
|
||||
case r.FormValue("mark") == "feed":
|
||||
c.handleWriteFeeds(w, r)
|
||||
case r.FormValue("mark") == "group":
|
||||
c.handleWriteGroups(w, r)
|
||||
default:
|
||||
response.JSON().Standard(newBaseResponse())
|
||||
json.OK(w, newBaseResponse())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,19 +179,20 @@ The “Sparks” super group is not included in this response and is composed of
|
||||
is_spark equal to 1.
|
||||
|
||||
*/
|
||||
func (c *Controller) handleGroups(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) handleGroups(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Fetching groups for userID=%d", userID)
|
||||
|
||||
categories, err := c.store.Categories(userID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
feeds, err := c.store.Feeds(userID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -197,7 +203,7 @@ func (c *Controller) handleGroups(ctx *handler.Context, request *handler.Request
|
||||
|
||||
result.FeedsGroups = c.buildFeedGroups(feeds)
|
||||
result.SetCommonValues()
|
||||
response.JSON().Standard(result)
|
||||
json.OK(w, result)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -224,13 +230,14 @@ should be limited to feeds with an is_spark equal to 0.
|
||||
|
||||
For the “Sparks” super group the items should be limited to feeds with an is_spark equal to 1.
|
||||
*/
|
||||
func (c *Controller) handleFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) handleFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Fetching feeds for userID=%d", userID)
|
||||
|
||||
feeds, err := c.store.Feeds(userID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -255,7 +262,7 @@ func (c *Controller) handleFeeds(ctx *handler.Context, request *handler.Request,
|
||||
|
||||
result.FeedsGroups = c.buildFeedGroups(feeds)
|
||||
result.SetCommonValues()
|
||||
response.JSON().Standard(result)
|
||||
json.OK(w, result)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -277,13 +284,14 @@ A PHP/HTML example:
|
||||
|
||||
echo '<img src="data:'.$favicon['data'].'">';
|
||||
*/
|
||||
func (c *Controller) handleFavicons(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) handleFavicons(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Fetching favicons for userID=%d", userID)
|
||||
|
||||
icons, err := c.store.Icons(userID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -296,7 +304,7 @@ func (c *Controller) handleFavicons(ctx *handler.Context, request *handler.Reque
|
||||
}
|
||||
|
||||
result.SetCommonValues()
|
||||
response.JSON().Standard(result)
|
||||
json.OK(w, result)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -330,9 +338,10 @@ Three optional arguments control determine the items included in the response.
|
||||
(added in API version 2)
|
||||
|
||||
*/
|
||||
func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) handleItems(w http.ResponseWriter, r *http.Request) {
|
||||
var result itemsResponse
|
||||
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Fetching items for userID=%d", userID)
|
||||
|
||||
@@ -342,17 +351,17 @@ func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request,
|
||||
builder.WithOrder("id")
|
||||
builder.WithDirection(model.DefaultSortingDirection)
|
||||
|
||||
sinceID := request.QueryIntegerParam("since_id", 0)
|
||||
sinceID := request.QueryIntParam(r, "since_id", 0)
|
||||
if sinceID > 0 {
|
||||
builder.WithGreaterThanEntryID(int64(sinceID))
|
||||
}
|
||||
|
||||
maxID := request.QueryIntegerParam("max_id", 0)
|
||||
maxID := request.QueryIntParam(r, "max_id", 0)
|
||||
if maxID > 0 {
|
||||
builder.WithOffset(maxID)
|
||||
}
|
||||
|
||||
csvItemIDs := request.QueryStringParam("with_ids", "")
|
||||
csvItemIDs := request.QueryParam(r, "with_ids", "")
|
||||
if csvItemIDs != "" {
|
||||
var itemIDs []int64
|
||||
|
||||
@@ -367,7 +376,7 @@ func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request,
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -375,10 +384,11 @@ func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request,
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
result.Total, err = builder.CountEntries()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
result.Items = make([]item, 0)
|
||||
for _, entry := range entries {
|
||||
isRead := 0
|
||||
if entry.Status == model.EntryStatusRead {
|
||||
@@ -404,7 +414,7 @@ func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request,
|
||||
}
|
||||
|
||||
result.SetCommonValues()
|
||||
response.JSON().Standard(result)
|
||||
json.OK(w, result)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -414,7 +424,8 @@ with the remote Fever installation.
|
||||
A request with the unread_item_ids argument will return one additional member:
|
||||
unread_item_ids (string/comma-separated list of positive integers)
|
||||
*/
|
||||
func (c *Controller) handleUnreadItems(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) handleUnreadItems(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Fetching unread items for userID=%d", userID)
|
||||
|
||||
@@ -422,7 +433,7 @@ func (c *Controller) handleUnreadItems(ctx *handler.Context, request *handler.Re
|
||||
builder.WithStatus(model.EntryStatusUnread)
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -434,7 +445,7 @@ func (c *Controller) handleUnreadItems(ctx *handler.Context, request *handler.Re
|
||||
var result unreadResponse
|
||||
result.ItemIDs = strings.Join(itemIDs, ",")
|
||||
result.SetCommonValues()
|
||||
response.JSON().Standard(result)
|
||||
json.OK(w, result)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -445,7 +456,8 @@ with the remote Fever installation.
|
||||
|
||||
saved_item_ids (string/comma-separated list of positive integers)
|
||||
*/
|
||||
func (c *Controller) handleSavedItems(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) handleSavedItems(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Fetching saved items for userID=%d", userID)
|
||||
|
||||
@@ -454,7 +466,7 @@ func (c *Controller) handleSavedItems(ctx *handler.Context, request *handler.Req
|
||||
|
||||
entryIDs, err := builder.GetEntryIDs()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -465,7 +477,7 @@ func (c *Controller) handleSavedItems(ctx *handler.Context, request *handler.Req
|
||||
|
||||
result := &savedResponse{ItemIDs: strings.Join(itemsIDs, ",")}
|
||||
result.SetCommonValues()
|
||||
response.JSON().Standard(result)
|
||||
json.OK(w, result)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -473,11 +485,12 @@ func (c *Controller) handleSavedItems(ctx *handler.Context, request *handler.Req
|
||||
as=? where ? is replaced with read, saved or unsaved
|
||||
id=? where ? is replaced with the id of the item to modify
|
||||
*/
|
||||
func (c *Controller) handleWriteItems(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) handleWriteItems(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Receiving mark=item call for userID=%d", userID)
|
||||
|
||||
entryID := request.FormIntegerValue("id")
|
||||
entryID := request.FormIntValue(r, "id")
|
||||
if entryID <= 0 {
|
||||
return
|
||||
}
|
||||
@@ -488,7 +501,7 @@ func (c *Controller) handleWriteItems(ctx *handler.Context, request *handler.Req
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -496,29 +509,32 @@ func (c *Controller) handleWriteItems(ctx *handler.Context, request *handler.Req
|
||||
return
|
||||
}
|
||||
|
||||
switch request.FormValue("as") {
|
||||
switch r.FormValue("as") {
|
||||
case "read":
|
||||
logger.Debug("[Fever] Mark entry #%d as read", entryID)
|
||||
c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead)
|
||||
case "unread":
|
||||
logger.Debug("[Fever] Mark entry #%d as unread", entryID)
|
||||
c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
|
||||
case "saved", "unsaved":
|
||||
logger.Debug("[Fever] Mark entry #%d as saved/unsaved", entryID)
|
||||
if err := c.store.ToggleBookmark(userID, entryID); err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := c.store.Integration(userID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
integration.SendEntry(entry, settings)
|
||||
integration.SendEntry(c.cfg, entry, settings)
|
||||
}()
|
||||
}
|
||||
|
||||
response.JSON().Standard(newBaseResponse())
|
||||
json.OK(w, newBaseResponse())
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -527,11 +543,12 @@ func (c *Controller) handleWriteItems(ctx *handler.Context, request *handler.Req
|
||||
id=? where ? is replaced with the id of the feed or group to modify
|
||||
before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request
|
||||
*/
|
||||
func (c *Controller) handleWriteFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) handleWriteFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Receiving mark=feed call for userID=%d", userID)
|
||||
|
||||
feedID := request.FormIntegerValue("id")
|
||||
feedID := request.FormIntValue(r, "id")
|
||||
if feedID <= 0 {
|
||||
return
|
||||
}
|
||||
@@ -540,7 +557,7 @@ func (c *Controller) handleWriteFeeds(ctx *handler.Context, request *handler.Req
|
||||
builder.WithStatus(model.EntryStatusUnread)
|
||||
builder.WithFeedID(feedID)
|
||||
|
||||
before := request.FormIntegerValue("before")
|
||||
before := request.FormIntValue(r, "before")
|
||||
if before > 0 {
|
||||
t := time.Unix(before, 0)
|
||||
builder.Before(&t)
|
||||
@@ -548,17 +565,17 @@ func (c *Controller) handleWriteFeeds(ctx *handler.Context, request *handler.Req
|
||||
|
||||
entryIDs, err := builder.GetEntryIDs()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(newBaseResponse())
|
||||
json.OK(w, newBaseResponse())
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -567,11 +584,12 @@ func (c *Controller) handleWriteFeeds(ctx *handler.Context, request *handler.Req
|
||||
id=? where ? is replaced with the id of the feed or group to modify
|
||||
before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request
|
||||
*/
|
||||
func (c *Controller) handleWriteGroups(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
func (c *Controller) handleWriteGroups(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Receiving mark=group call for userID=%d", userID)
|
||||
|
||||
groupID := request.FormIntegerValue("id")
|
||||
groupID := request.FormIntValue(r, "id")
|
||||
if groupID < 0 {
|
||||
return
|
||||
}
|
||||
@@ -580,7 +598,7 @@ func (c *Controller) handleWriteGroups(ctx *handler.Context, request *handler.Re
|
||||
builder.WithStatus(model.EntryStatusUnread)
|
||||
builder.WithCategoryID(groupID)
|
||||
|
||||
before := request.FormIntegerValue("before")
|
||||
before := request.FormIntValue(r, "before")
|
||||
if before > 0 {
|
||||
t := time.Unix(before, 0)
|
||||
builder.Before(&t)
|
||||
@@ -588,17 +606,17 @@ func (c *Controller) handleWriteGroups(ctx *handler.Context, request *handler.Re
|
||||
|
||||
entryIDs, err := builder.GetEntryIDs()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(newBaseResponse())
|
||||
json.OK(w, newBaseResponse())
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -626,6 +644,6 @@ func (c *Controller) buildFeedGroups(feeds model.Feeds) []feedsGroups {
|
||||
}
|
||||
|
||||
// NewController returns a new Fever API.
|
||||
func NewController(store *storage.Storage) *Controller {
|
||||
return &Controller{store: store}
|
||||
func NewController(cfg *config.Config, store *storage.Storage) *Controller {
|
||||
return &Controller{cfg, store}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package http
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -49,6 +50,26 @@ type Client struct {
|
||||
Insecure bool
|
||||
}
|
||||
|
||||
// WithCredentials defines the username/password for HTTP Basic authentication.
|
||||
func (c *Client) WithCredentials(username, password string) *Client {
|
||||
c.username = username
|
||||
c.password = password
|
||||
return c
|
||||
}
|
||||
|
||||
// WithAuthorization defines authorization header value.
|
||||
func (c *Client) WithAuthorization(authorization string) *Client {
|
||||
c.authorizationHeader = authorization
|
||||
return c
|
||||
}
|
||||
|
||||
// WithCacheHeaders defines caching headers.
|
||||
func (c *Client) WithCacheHeaders(etagHeader, lastModifiedHeader string) *Client {
|
||||
c.etagHeader = etagHeader
|
||||
c.lastModifiedHeader = lastModifiedHeader
|
||||
return c
|
||||
}
|
||||
|
||||
// Get execute a GET HTTP request.
|
||||
func (c *Client) Get() (*Response, error) {
|
||||
request, err := c.buildRequest(http.MethodGet, nil)
|
||||
@@ -114,13 +135,19 @@ func (c *Client) executeRequest(request *http.Request) (*Response, error) {
|
||||
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.ContentLength > maxBodySize {
|
||||
return nil, fmt.Errorf("client: response too large (%d bytes)", resp.ContentLength)
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("client: error while reading body %v", err)
|
||||
}
|
||||
|
||||
response := &Response{
|
||||
Body: resp.Body,
|
||||
Body: bytes.NewReader(buf),
|
||||
StatusCode: resp.StatusCode,
|
||||
EffectiveURL: resp.Request.URL.String(),
|
||||
LastModified: resp.Header.Get("Last-Modified"),
|
||||
@@ -197,22 +224,7 @@ func (c *Client) buildHeaders() http.Header {
|
||||
return headers
|
||||
}
|
||||
|
||||
// NewClient returns a new HTTP client.
|
||||
func NewClient(url string) *Client {
|
||||
// New returns a new HTTP client.
|
||||
func New(url string) *Client {
|
||||
return &Client{url: url, Insecure: false}
|
||||
}
|
||||
|
||||
// NewClientWithCredentials returns a new HTTP client that requires authentication.
|
||||
func NewClientWithCredentials(url, username, password string) *Client {
|
||||
return &Client{url: url, Insecure: false, username: username, password: password}
|
||||
}
|
||||
|
||||
// NewClientWithAuthorization returns a new client with a custom authorization header.
|
||||
func NewClientWithAuthorization(url, authorization string) *Client {
|
||||
return &Client{url: url, Insecure: false, authorizationHeader: authorization}
|
||||
}
|
||||
|
||||
// NewClientWithCacheHeaders returns a new HTTP client that send cache headers.
|
||||
func NewClientWithCacheHeaders(url, etagHeader, lastModifiedHeader string) *Client {
|
||||
return &Client{url: url, etagHeader: etagHeader, lastModifiedHeader: lastModifiedHeader, Insecure: false}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package http
|
||||
package client
|
||||
|
||||
import (
|
||||
"io"
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package http
|
||||
package client
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/middleware"
|
||||
)
|
||||
|
||||
// Context contains helper functions related to the current request.
|
||||
type Context struct {
|
||||
request *http.Request
|
||||
}
|
||||
|
||||
// IsAdminUser checks if the logged user is administrator.
|
||||
func (c *Context) IsAdminUser() bool {
|
||||
return c.getContextBoolValue(middleware.IsAdminUserContextKey)
|
||||
}
|
||||
|
||||
// IsAuthenticated returns a boolean if the user is authenticated.
|
||||
func (c *Context) IsAuthenticated() bool {
|
||||
return c.getContextBoolValue(middleware.IsAuthenticatedContextKey)
|
||||
}
|
||||
|
||||
// UserID returns the UserID of the logged user.
|
||||
func (c *Context) UserID() int64 {
|
||||
return c.getContextIntValue(middleware.UserIDContextKey)
|
||||
}
|
||||
|
||||
// UserTimezone returns the timezone used by the logged user.
|
||||
func (c *Context) UserTimezone() string {
|
||||
value := c.getContextStringValue(middleware.UserTimezoneContextKey)
|
||||
if value == "" {
|
||||
value = "UTC"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// UserLanguage get the locale used by the current logged user.
|
||||
func (c *Context) UserLanguage() string {
|
||||
language := c.getContextStringValue(middleware.UserLanguageContextKey)
|
||||
if language == "" {
|
||||
language = "en_US"
|
||||
}
|
||||
return language
|
||||
}
|
||||
|
||||
// CSRF returns the current CSRF token.
|
||||
func (c *Context) CSRF() string {
|
||||
return c.getContextStringValue(middleware.CSRFContextKey)
|
||||
}
|
||||
|
||||
// SessionID returns the current session ID.
|
||||
func (c *Context) SessionID() string {
|
||||
return c.getContextStringValue(middleware.SessionIDContextKey)
|
||||
}
|
||||
|
||||
// UserSessionToken returns the current user session token.
|
||||
func (c *Context) UserSessionToken() string {
|
||||
return c.getContextStringValue(middleware.UserSessionTokenContextKey)
|
||||
}
|
||||
|
||||
// OAuth2State returns the current OAuth2 state.
|
||||
func (c *Context) OAuth2State() string {
|
||||
return c.getContextStringValue(middleware.OAuth2StateContextKey)
|
||||
}
|
||||
|
||||
// FlashMessage returns the message message if any.
|
||||
func (c *Context) FlashMessage() string {
|
||||
return c.getContextStringValue(middleware.FlashMessageContextKey)
|
||||
}
|
||||
|
||||
// FlashErrorMessage returns the message error message if any.
|
||||
func (c *Context) FlashErrorMessage() string {
|
||||
return c.getContextStringValue(middleware.FlashErrorMessageContextKey)
|
||||
}
|
||||
|
||||
// PocketRequestToken returns the Pocket Request Token if any.
|
||||
func (c *Context) PocketRequestToken() string {
|
||||
return c.getContextStringValue(middleware.PocketRequestTokenContextKey)
|
||||
}
|
||||
|
||||
func (c *Context) getContextStringValue(key *middleware.ContextKey) string {
|
||||
if v := c.request.Context().Value(key); v != nil {
|
||||
return v.(string)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Context) getContextBoolValue(key *middleware.ContextKey) bool {
|
||||
if v := c.request.Context().Value(key); v != nil {
|
||||
return v.(bool)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Context) getContextIntValue(key *middleware.ContextKey) int64 {
|
||||
if v := c.request.Context().Value(key); v != nil {
|
||||
return v.(int64)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// New creates a new Context.
|
||||
func New(r *http.Request) *Context {
|
||||
return &Context{r}
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package http implements a set of utilities related to the HTTP protocol.
|
||||
|
||||
*/
|
||||
package http
|
||||
@@ -1,163 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/crypto"
|
||||
"github.com/miniflux/miniflux/http/middleware"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Context contains helper functions related to the current request.
|
||||
type Context struct {
|
||||
writer http.ResponseWriter
|
||||
request *http.Request
|
||||
store *storage.Storage
|
||||
router *mux.Router
|
||||
user *model.User
|
||||
translator *locale.Translator
|
||||
}
|
||||
|
||||
// IsAdminUser checks if the logged user is administrator.
|
||||
func (c *Context) IsAdminUser() bool {
|
||||
if v := c.request.Context().Value(middleware.IsAdminUserContextKey); v != nil {
|
||||
return v.(bool)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UserTimezone returns the timezone used by the logged user.
|
||||
func (c *Context) UserTimezone() string {
|
||||
value := c.getContextStringValue(middleware.UserTimezoneContextKey)
|
||||
if value == "" {
|
||||
value = "UTC"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// IsAuthenticated returns a boolean if the user is authenticated.
|
||||
func (c *Context) IsAuthenticated() bool {
|
||||
if v := c.request.Context().Value(middleware.IsAuthenticatedContextKey); v != nil {
|
||||
return v.(bool)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UserID returns the UserID of the logged user.
|
||||
func (c *Context) UserID() int64 {
|
||||
if v := c.request.Context().Value(middleware.UserIDContextKey); v != nil {
|
||||
return v.(int64)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// LoggedUser returns all properties related to the logged user.
|
||||
func (c *Context) LoggedUser() *model.User {
|
||||
if c.user == nil {
|
||||
var err error
|
||||
c.user, err = c.store.UserByID(c.UserID())
|
||||
if err != nil {
|
||||
logger.Fatal("[Context] %v", err)
|
||||
}
|
||||
|
||||
if c.user == nil {
|
||||
logger.Fatal("Unable to find user from context")
|
||||
}
|
||||
}
|
||||
|
||||
return c.user
|
||||
}
|
||||
|
||||
// UserLanguage get the locale used by the current logged user.
|
||||
func (c *Context) UserLanguage() string {
|
||||
if c.IsAuthenticated() {
|
||||
user := c.LoggedUser()
|
||||
return user.Language
|
||||
}
|
||||
|
||||
return c.getContextStringValue(middleware.UserLanguageContextKey)
|
||||
}
|
||||
|
||||
// Translate translates a message in the current language.
|
||||
func (c *Context) Translate(message string, args ...interface{}) string {
|
||||
return c.translator.GetLanguage(c.UserLanguage()).Get(message, args...)
|
||||
}
|
||||
|
||||
// CSRF returns the current CSRF token.
|
||||
func (c *Context) CSRF() string {
|
||||
return c.getContextStringValue(middleware.CSRFContextKey)
|
||||
}
|
||||
|
||||
// SessionID returns the current session ID.
|
||||
func (c *Context) SessionID() string {
|
||||
return c.getContextStringValue(middleware.SessionIDContextKey)
|
||||
}
|
||||
|
||||
// UserSessionToken returns the current user session token.
|
||||
func (c *Context) UserSessionToken() string {
|
||||
return c.getContextStringValue(middleware.UserSessionTokenContextKey)
|
||||
}
|
||||
|
||||
// OAuth2State returns the current OAuth2 state.
|
||||
func (c *Context) OAuth2State() string {
|
||||
return c.getContextStringValue(middleware.OAuth2StateContextKey)
|
||||
}
|
||||
|
||||
// GenerateOAuth2State generate a new OAuth2 state.
|
||||
func (c *Context) GenerateOAuth2State() string {
|
||||
state := crypto.GenerateRandomString(32)
|
||||
c.store.UpdateSessionField(c.SessionID(), "oauth2_state", state)
|
||||
return state
|
||||
}
|
||||
|
||||
// SetFlashMessage defines a new flash message.
|
||||
func (c *Context) SetFlashMessage(message string) {
|
||||
c.store.UpdateSessionField(c.SessionID(), "flash_message", message)
|
||||
}
|
||||
|
||||
// FlashMessage returns the flash message and remove it.
|
||||
func (c *Context) FlashMessage() string {
|
||||
message := c.getContextStringValue(middleware.FlashMessageContextKey)
|
||||
c.store.UpdateSessionField(c.SessionID(), "flash_message", "")
|
||||
return message
|
||||
}
|
||||
|
||||
// SetFlashErrorMessage defines a new flash error message.
|
||||
func (c *Context) SetFlashErrorMessage(message string) {
|
||||
c.store.UpdateSessionField(c.SessionID(), "flash_error_message", message)
|
||||
}
|
||||
|
||||
// FlashErrorMessage returns the error flash message and remove it.
|
||||
func (c *Context) FlashErrorMessage() string {
|
||||
message := c.getContextStringValue(middleware.FlashErrorMessageContextKey)
|
||||
c.store.UpdateSessionField(c.SessionID(), "flash_error_message", "")
|
||||
return message
|
||||
}
|
||||
|
||||
func (c *Context) getContextStringValue(key *middleware.ContextKey) string {
|
||||
if v := c.request.Context().Value(key); v != nil {
|
||||
return v.(string)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Route returns the path for the given arguments.
|
||||
func (c *Context) Route(name string, args ...interface{}) string {
|
||||
return route.Path(c.router, name, args...)
|
||||
}
|
||||
|
||||
// NewContext creates a new Context.
|
||||
func NewContext(r *http.Request, store *storage.Storage, router *mux.Router, translator *locale.Translator) *Context {
|
||||
return &Context{request: r, store: store, router: router, translator: translator}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/config"
|
||||
"github.com/miniflux/miniflux/http/middleware"
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
"github.com/miniflux/miniflux/template"
|
||||
"github.com/miniflux/miniflux/timer"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/tomasen/realip"
|
||||
)
|
||||
|
||||
// ControllerFunc is an application HTTP handler.
|
||||
type ControllerFunc func(ctx *Context, request *Request, response *Response)
|
||||
|
||||
// Handler manages HTTP handlers and middlewares.
|
||||
type Handler struct {
|
||||
cfg *config.Config
|
||||
store *storage.Storage
|
||||
translator *locale.Translator
|
||||
template *template.Engine
|
||||
router *mux.Router
|
||||
middleware *middleware.Chain
|
||||
}
|
||||
|
||||
// Use is a wrapper around an HTTP handler.
|
||||
func (h *Handler) Use(f ControllerFunc) http.Handler {
|
||||
return h.middleware.WrapFunc(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer timer.ExecutionTime(time.Now(), r.URL.Path)
|
||||
logger.Debug("[HTTP] %s %s %s", realip.RealIP(r), r.Method, r.URL.Path)
|
||||
|
||||
if r.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
h.cfg.IsHTTPS = true
|
||||
}
|
||||
|
||||
ctx := NewContext(r, h.store, h.router, h.translator)
|
||||
request := NewRequest(r)
|
||||
response := NewResponse(h.cfg, w, r, h.template)
|
||||
language := ctx.UserLanguage()
|
||||
|
||||
if language != "" {
|
||||
h.template.SetLanguage(language)
|
||||
} else {
|
||||
h.template.SetLanguage("en_US")
|
||||
}
|
||||
|
||||
f(ctx, request, response)
|
||||
}))
|
||||
}
|
||||
|
||||
// NewHandler returns a new Handler.
|
||||
func NewHandler(cfg *config.Config, store *storage.Storage, router *mux.Router, template *template.Engine, translator *locale.Translator, middleware *middleware.Chain) *Handler {
|
||||
return &Handler{
|
||||
cfg: cfg,
|
||||
store: store,
|
||||
translator: translator,
|
||||
router: router,
|
||||
template: template,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/template"
|
||||
)
|
||||
|
||||
// HTMLResponse handles HTML responses.
|
||||
type HTMLResponse struct {
|
||||
writer http.ResponseWriter
|
||||
request *http.Request
|
||||
template *template.Engine
|
||||
}
|
||||
|
||||
// Render execute a template and send to the client the generated HTML.
|
||||
func (h *HTMLResponse) Render(template string, args map[string]interface{}) {
|
||||
h.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
h.template.Execute(h.writer, template, args)
|
||||
}
|
||||
|
||||
// ServerError sends a 500 error to the browser.
|
||||
func (h *HTMLResponse) ServerError(err error) {
|
||||
h.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
h.writer.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
if err != nil {
|
||||
logger.Error("[Internal Server Error] %v", err)
|
||||
h.writer.Write([]byte("Internal Server Error: " + err.Error()))
|
||||
} else {
|
||||
h.writer.Write([]byte("Internal Server Error"))
|
||||
}
|
||||
}
|
||||
|
||||
// BadRequest sends a 400 error to the browser.
|
||||
func (h *HTMLResponse) BadRequest(err error) {
|
||||
h.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
h.writer.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
if err != nil {
|
||||
logger.Error("[Bad Request] %v", err)
|
||||
h.writer.Write([]byte("Bad Request: " + err.Error()))
|
||||
} else {
|
||||
h.writer.Write([]byte("Bad Request"))
|
||||
}
|
||||
}
|
||||
|
||||
// NotFound sends a 404 error to the browser.
|
||||
func (h *HTMLResponse) NotFound() {
|
||||
h.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
h.writer.WriteHeader(http.StatusNotFound)
|
||||
h.writer.Write([]byte("Page Not Found"))
|
||||
}
|
||||
|
||||
// Forbidden sends a 403 error to the browser.
|
||||
func (h *HTMLResponse) Forbidden() {
|
||||
h.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
h.writer.WriteHeader(http.StatusForbidden)
|
||||
h.writer.Write([]byte("Access Forbidden"))
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
)
|
||||
|
||||
// JSONResponse handles JSON responses.
|
||||
type JSONResponse struct {
|
||||
writer http.ResponseWriter
|
||||
request *http.Request
|
||||
}
|
||||
|
||||
// Standard sends a JSON response with the status code 200.
|
||||
func (j *JSONResponse) Standard(v interface{}) {
|
||||
j.commonHeaders()
|
||||
j.writer.WriteHeader(http.StatusOK)
|
||||
j.writer.Write(j.toJSON(v))
|
||||
}
|
||||
|
||||
// Created sends a JSON response with the status code 201.
|
||||
func (j *JSONResponse) Created(v interface{}) {
|
||||
j.commonHeaders()
|
||||
j.writer.WriteHeader(http.StatusCreated)
|
||||
j.writer.Write(j.toJSON(v))
|
||||
}
|
||||
|
||||
// NoContent sends a JSON response with the status code 204.
|
||||
func (j *JSONResponse) NoContent() {
|
||||
j.commonHeaders()
|
||||
j.writer.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// BadRequest sends a JSON response with the status code 400.
|
||||
func (j *JSONResponse) BadRequest(err error) {
|
||||
logger.Error("[Bad Request] %v", err)
|
||||
j.commonHeaders()
|
||||
j.writer.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
if err != nil {
|
||||
j.writer.Write(j.encodeError(err))
|
||||
}
|
||||
}
|
||||
|
||||
// NotFound sends a JSON response with the status code 404.
|
||||
func (j *JSONResponse) NotFound(err error) {
|
||||
logger.Error("[Not Found] %v", err)
|
||||
j.commonHeaders()
|
||||
j.writer.WriteHeader(http.StatusNotFound)
|
||||
j.writer.Write(j.encodeError(err))
|
||||
}
|
||||
|
||||
// ServerError sends a JSON response with the status code 500.
|
||||
func (j *JSONResponse) ServerError(err error) {
|
||||
logger.Error("[Internal Server Error] %v", err)
|
||||
j.commonHeaders()
|
||||
j.writer.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
if err != nil {
|
||||
j.writer.Write(j.encodeError(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Forbidden sends a JSON response with the status code 403.
|
||||
func (j *JSONResponse) Forbidden() {
|
||||
logger.Info("[API:Forbidden]")
|
||||
j.commonHeaders()
|
||||
j.writer.WriteHeader(http.StatusForbidden)
|
||||
j.writer.Write(j.encodeError(errors.New("Access Forbidden")))
|
||||
}
|
||||
|
||||
func (j *JSONResponse) commonHeaders() {
|
||||
j.writer.Header().Set("Accept", "application/json")
|
||||
j.writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
}
|
||||
|
||||
func (j *JSONResponse) encodeError(err error) []byte {
|
||||
type errorMsg struct {
|
||||
ErrorMessage string `json:"error_message"`
|
||||
}
|
||||
|
||||
tmp := errorMsg{ErrorMessage: err.Error()}
|
||||
data, err := json.Marshal(tmp)
|
||||
if err != nil {
|
||||
logger.Error("encoding error: %v", err)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (j *JSONResponse) toJSON(v interface{}) []byte {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
logger.Error("encoding error: %v", err)
|
||||
return []byte("")
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// NewJSONResponse returns a new JSONResponse.
|
||||
func NewJSONResponse(w http.ResponseWriter, r *http.Request) *JSONResponse {
|
||||
return &JSONResponse{request: r, writer: w}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
)
|
||||
|
||||
// Request is a thin wrapper around "http.Request".
|
||||
type Request struct {
|
||||
request *http.Request
|
||||
}
|
||||
|
||||
// Request returns the raw Request struct.
|
||||
func (r *Request) Request() *http.Request {
|
||||
return r.request
|
||||
}
|
||||
|
||||
// Body returns the request body.
|
||||
func (r *Request) Body() io.ReadCloser {
|
||||
return r.request.Body
|
||||
}
|
||||
|
||||
// File returns uploaded file properties.
|
||||
func (r *Request) File(name string) (multipart.File, *multipart.FileHeader, error) {
|
||||
return r.request.FormFile(name)
|
||||
}
|
||||
|
||||
// Cookie returns the cookie value.
|
||||
func (r *Request) Cookie(name string) string {
|
||||
cookie, err := r.request.Cookie(name)
|
||||
if err == http.ErrNoCookie {
|
||||
return ""
|
||||
}
|
||||
|
||||
return cookie.Value
|
||||
}
|
||||
|
||||
// FormValue returns a form value as integer.
|
||||
func (r *Request) FormValue(param string) string {
|
||||
return r.request.FormValue(param)
|
||||
}
|
||||
|
||||
// FormIntegerValue returns a form value as integer.
|
||||
func (r *Request) FormIntegerValue(param string) int64 {
|
||||
value := r.request.FormValue(param)
|
||||
integer, _ := strconv.Atoi(value)
|
||||
return int64(integer)
|
||||
}
|
||||
|
||||
// IntegerParam returns an URL parameter as integer.
|
||||
func (r *Request) IntegerParam(param string) (int64, error) {
|
||||
vars := mux.Vars(r.request)
|
||||
value, err := strconv.Atoi(vars[param])
|
||||
if err != nil {
|
||||
logger.Error("[IntegerParam] %v", err)
|
||||
return 0, fmt.Errorf("%s parameter is not an integer", param)
|
||||
}
|
||||
|
||||
if value < 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return int64(value), nil
|
||||
}
|
||||
|
||||
// StringParam returns an URL parameter as string.
|
||||
func (r *Request) StringParam(param, defaultValue string) string {
|
||||
vars := mux.Vars(r.request)
|
||||
value := vars[param]
|
||||
if value == "" {
|
||||
value = defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// QueryStringParam returns a querystring parameter as string.
|
||||
func (r *Request) QueryStringParam(param, defaultValue string) string {
|
||||
value := r.request.URL.Query().Get(param)
|
||||
if value == "" {
|
||||
value = defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// QueryIntegerParam returns a querystring parameter as string.
|
||||
func (r *Request) QueryIntegerParam(param string, defaultValue int) int {
|
||||
value := r.request.URL.Query().Get(param)
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
val, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
if val < 0 {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// HasQueryParam checks if the query string contains the given parameter.
|
||||
func (r *Request) HasQueryParam(param string) bool {
|
||||
values := r.request.URL.Query()
|
||||
_, ok := values[param]
|
||||
return ok
|
||||
}
|
||||
|
||||
// NewRequest returns a new Request.
|
||||
func NewRequest(r *http.Request) *Request {
|
||||
return &Request{r}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/config"
|
||||
"github.com/miniflux/miniflux/template"
|
||||
)
|
||||
|
||||
// Response handles HTTP responses.
|
||||
type Response struct {
|
||||
cfg *config.Config
|
||||
writer http.ResponseWriter
|
||||
request *http.Request
|
||||
template *template.Engine
|
||||
}
|
||||
|
||||
// SetCookie send a cookie to the client.
|
||||
func (r *Response) SetCookie(cookie *http.Cookie) {
|
||||
http.SetCookie(r.writer, cookie)
|
||||
}
|
||||
|
||||
// JSON returns a JSONResponse.
|
||||
func (r *Response) JSON() *JSONResponse {
|
||||
r.commonHeaders()
|
||||
return NewJSONResponse(r.writer, r.request)
|
||||
}
|
||||
|
||||
// HTML returns a HTMLResponse.
|
||||
func (r *Response) HTML() *HTMLResponse {
|
||||
r.commonHeaders()
|
||||
return &HTMLResponse{writer: r.writer, request: r.request, template: r.template}
|
||||
}
|
||||
|
||||
// XML returns a XMLResponse.
|
||||
func (r *Response) XML() *XMLResponse {
|
||||
r.commonHeaders()
|
||||
return &XMLResponse{writer: r.writer, request: r.request}
|
||||
}
|
||||
|
||||
// Redirect redirects the user to another location.
|
||||
func (r *Response) Redirect(path string) {
|
||||
http.Redirect(r.writer, r.request, path, http.StatusFound)
|
||||
}
|
||||
|
||||
// NotModified sends a response with a 304 status code.
|
||||
func (r *Response) NotModified() {
|
||||
r.commonHeaders()
|
||||
r.writer.WriteHeader(http.StatusNotModified)
|
||||
}
|
||||
|
||||
// Cache returns a response with caching headers.
|
||||
func (r *Response) Cache(mimeType, etag string, content []byte, duration time.Duration) {
|
||||
r.writer.Header().Set("Content-Type", mimeType)
|
||||
r.writer.Header().Set("ETag", etag)
|
||||
r.writer.Header().Set("Cache-Control", "public")
|
||||
r.writer.Header().Set("Expires", time.Now().Add(duration).Format(time.RFC1123))
|
||||
|
||||
if etag == r.request.Header.Get("If-None-Match") {
|
||||
r.writer.WriteHeader(http.StatusNotModified)
|
||||
} else {
|
||||
r.writer.Write(content)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Response) commonHeaders() {
|
||||
r.writer.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||
r.writer.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
r.writer.Header().Set("X-Frame-Options", "DENY")
|
||||
|
||||
// Even if the directive "frame-src" has been deprecated in Firefox,
|
||||
// we keep it to stay compatible with other browsers.
|
||||
r.writer.Header().Set("Content-Security-Policy", "default-src 'self'; img-src *; media-src *; frame-src *; child-src *")
|
||||
|
||||
if r.cfg.IsHTTPS && r.cfg.HasHSTS() {
|
||||
r.writer.Header().Set("Strict-Transport-Security", "max-age=31536000")
|
||||
}
|
||||
}
|
||||
|
||||
// NewResponse returns a new Response.
|
||||
func NewResponse(cfg *config.Config, w http.ResponseWriter, r *http.Request, template *template.Engine) *Response {
|
||||
return &Response{cfg: cfg, writer: w, request: r, template: template}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// XMLResponse handles XML responses.
|
||||
type XMLResponse struct {
|
||||
writer http.ResponseWriter
|
||||
request *http.Request
|
||||
}
|
||||
|
||||
// Download force the download of a XML document.
|
||||
func (x *XMLResponse) Download(filename, data string) {
|
||||
x.writer.Header().Set("Content-Type", "text/xml")
|
||||
x.writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
||||
x.writer.Write([]byte(data))
|
||||
}
|
||||
|
||||
// Serve forces the XML to be sent to browser.
|
||||
func (x *XMLResponse) Serve(data string) {
|
||||
x.writer.Header().Set("Content-Type", "text/xml")
|
||||
x.writer.Write([]byte(data))
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Middleware represents a HTTP middleware.
|
||||
type Middleware func(http.Handler) http.Handler
|
||||
|
||||
// Chain handles a list of middlewares.
|
||||
type Chain struct {
|
||||
middlewares []Middleware
|
||||
}
|
||||
|
||||
// Wrap adds a HTTP handler into the chain.
|
||||
func (m *Chain) Wrap(h http.Handler) http.Handler {
|
||||
for i := range m.middlewares {
|
||||
h = m.middlewares[len(m.middlewares)-1-i](h)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// WrapFunc adds a HTTP handler function into the chain.
|
||||
func (m *Chain) WrapFunc(fn http.HandlerFunc) http.Handler {
|
||||
return m.Wrap(fn)
|
||||
}
|
||||
|
||||
// NewChain returns a new Chain.
|
||||
func NewChain(middlewares ...Middleware) *Chain {
|
||||
return &Chain{append(([]Middleware)(nil), middlewares...)}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/config"
|
||||
"github.com/miniflux/miniflux/http/cookie"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
)
|
||||
|
||||
// SessionMiddleware represents a session middleware.
|
||||
type SessionMiddleware struct {
|
||||
cfg *config.Config
|
||||
store *storage.Storage
|
||||
}
|
||||
|
||||
// Handler execute the middleware.
|
||||
func (s *SessionMiddleware) Handler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
session := s.getSessionValueFromCookie(r)
|
||||
|
||||
if session == nil {
|
||||
logger.Debug("[Middleware:Session] Session not found")
|
||||
session, err = s.store.CreateSession()
|
||||
if err != nil {
|
||||
logger.Error("[Middleware:Session] %v", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie.New(cookie.CookieSessionID, session.ID, s.cfg.IsHTTPS, s.cfg.BasePath()))
|
||||
} else {
|
||||
logger.Debug("[Middleware:Session] %s", session)
|
||||
}
|
||||
|
||||
if r.Method == "POST" {
|
||||
formValue := r.FormValue("csrf")
|
||||
headerValue := r.Header.Get("X-Csrf-Token")
|
||||
|
||||
if session.Data.CSRF != formValue && session.Data.CSRF != headerValue {
|
||||
logger.Error(`[Middleware:Session] Invalid or missing CSRF token: Form="%s", Header="%s"`, formValue, headerValue)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Invalid or missing CSRF session!"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, SessionIDContextKey, session.ID)
|
||||
ctx = context.WithValue(ctx, CSRFContextKey, session.Data.CSRF)
|
||||
ctx = context.WithValue(ctx, OAuth2StateContextKey, session.Data.OAuth2State)
|
||||
ctx = context.WithValue(ctx, FlashMessageContextKey, session.Data.FlashMessage)
|
||||
ctx = context.WithValue(ctx, FlashErrorMessageContextKey, session.Data.FlashErrorMessage)
|
||||
ctx = context.WithValue(ctx, UserLanguageContextKey, session.Data.Language)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SessionMiddleware) getSessionValueFromCookie(r *http.Request) *model.Session {
|
||||
sessionCookie, err := r.Cookie(cookie.CookieSessionID)
|
||||
if err == http.ErrNoCookie {
|
||||
return nil
|
||||
}
|
||||
|
||||
session, err := s.store.Session(sessionCookie.Value)
|
||||
if err != nil {
|
||||
logger.Error("[Middleware:Session] %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
// NewSessionMiddleware returns a new SessionMiddleware.
|
||||
func NewSessionMiddleware(cfg *config.Config, store *storage.Storage) *SessionMiddleware {
|
||||
return &SessionMiddleware{cfg, store}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Cookie returns the cookie value.
|
||||
func Cookie(r *http.Request, name string) string {
|
||||
cookie, err := r.Cookie(name)
|
||||
if err == http.ErrNoCookie {
|
||||
return ""
|
||||
}
|
||||
|
||||
return cookie.Value
|
||||
}
|
||||
|
||||
// FormIntValue returns a form value as integer.
|
||||
func FormIntValue(r *http.Request, param string) int64 {
|
||||
value := r.FormValue(param)
|
||||
integer, _ := strconv.Atoi(value)
|
||||
return int64(integer)
|
||||
}
|
||||
|
||||
// IntParam returns an URL route parameter as integer.
|
||||
func IntParam(r *http.Request, param string) (int64, error) {
|
||||
vars := mux.Vars(r)
|
||||
value, err := strconv.Atoi(vars[param])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("request: %s parameter is not an integer", param)
|
||||
}
|
||||
|
||||
if value < 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return int64(value), nil
|
||||
}
|
||||
|
||||
// Param returns an URL route parameter as string.
|
||||
func Param(r *http.Request, param, defaultValue string) string {
|
||||
vars := mux.Vars(r)
|
||||
value := vars[param]
|
||||
if value == "" {
|
||||
value = defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// QueryParam returns a querystring parameter as string.
|
||||
func QueryParam(r *http.Request, param, defaultValue string) string {
|
||||
value := r.URL.Query().Get(param)
|
||||
if value == "" {
|
||||
value = defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// QueryIntParam returns a querystring parameter as integer.
|
||||
func QueryIntParam(r *http.Request, param string, defaultValue int) int {
|
||||
value := r.URL.Query().Get(param)
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
val, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
if val < 0 {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// HasQueryParam checks if the query string contains the given parameter.
|
||||
func HasQueryParam(r *http.Request, param string) bool {
|
||||
values := r.URL.Query()
|
||||
_, ok := values[param]
|
||||
return ok
|
||||
}
|
||||
|
||||
// RealIP returns client's real IP address.
|
||||
func RealIP(r *http.Request) string {
|
||||
headers := []string{"X-Forwarded-For", "X-Real-Ip"}
|
||||
for _, header := range headers {
|
||||
value := r.Header.Get(header)
|
||||
|
||||
if value != "" {
|
||||
addresses := strings.Split(value, ",")
|
||||
address := strings.TrimSpace(addresses[0])
|
||||
|
||||
if net.ParseIP(address) != nil {
|
||||
return address
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to TCP/IP source IP address.
|
||||
var remoteIP string
|
||||
if strings.ContainsRune(r.RemoteAddr, ':') {
|
||||
remoteIP, _, _ = net.SplitHostPort(r.RemoteAddr)
|
||||
} else {
|
||||
remoteIP = r.RemoteAddr
|
||||
}
|
||||
|
||||
return remoteIP
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRealIPWithoutHeaders(t *testing.T) {
|
||||
r := &http.Request{RemoteAddr: "192.168.0.1:4242"}
|
||||
if ip := RealIP(r); ip != "192.168.0.1" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
|
||||
r = &http.Request{RemoteAddr: "192.168.0.1"}
|
||||
if ip := RealIP(r); ip != "192.168.0.1" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealIPWithXFFHeader(t *testing.T) {
|
||||
// Test with multiple IPv4 addresses.
|
||||
headers := http.Header{}
|
||||
headers.Set("X-Forwarded-For", "203.0.113.195, 70.41.3.18, 150.172.238.178")
|
||||
r := &http.Request{RemoteAddr: "192.168.0.1:4242", Header: headers}
|
||||
|
||||
if ip := RealIP(r); ip != "203.0.113.195" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
|
||||
// Test with single IPv6 address.
|
||||
headers = http.Header{}
|
||||
headers.Set("X-Forwarded-For", "2001:db8:85a3:8d3:1319:8a2e:370:7348")
|
||||
r = &http.Request{RemoteAddr: "192.168.0.1:4242", Header: headers}
|
||||
|
||||
if ip := RealIP(r); ip != "2001:db8:85a3:8d3:1319:8a2e:370:7348" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
|
||||
// Test with single IPv4 address.
|
||||
headers = http.Header{}
|
||||
headers.Set("X-Forwarded-For", "70.41.3.18")
|
||||
r = &http.Request{RemoteAddr: "192.168.0.1:4242", Header: headers}
|
||||
|
||||
if ip := RealIP(r); ip != "70.41.3.18" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
|
||||
// Test with invalid IP address.
|
||||
headers = http.Header{}
|
||||
headers.Set("X-Forwarded-For", "fake IP")
|
||||
r = &http.Request{RemoteAddr: "192.168.0.1:4242", Header: headers}
|
||||
|
||||
if ip := RealIP(r); ip != "192.168.0.1" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealIPWithXRealIPHeader(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
headers.Set("X-Real-Ip", "192.168.122.1")
|
||||
r := &http.Request{RemoteAddr: "192.168.0.1:4242", Header: headers}
|
||||
|
||||
if ip := RealIP(r); ip != "192.168.122.1" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealIPWithBothHeaders(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
headers.Set("X-Forwarded-For", "203.0.113.195, 70.41.3.18, 150.172.238.178")
|
||||
headers.Set("X-Real-Ip", "192.168.122.1")
|
||||
|
||||
r := &http.Request{RemoteAddr: "192.168.0.1:4242", Header: headers}
|
||||
|
||||
if ip := RealIP(r); ip != "203.0.113.195" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package html
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
)
|
||||
|
||||
// OK writes a standard HTML response.
|
||||
func OK(w http.ResponseWriter, b []byte) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
// ServerError sends a 500 error to the browser.
|
||||
func ServerError(w http.ResponseWriter, err error) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
if err != nil {
|
||||
logger.Error("[Internal Server Error] %v", err)
|
||||
w.Write([]byte("Internal Server Error: " + err.Error()))
|
||||
} else {
|
||||
w.Write([]byte("Internal Server Error"))
|
||||
}
|
||||
}
|
||||
|
||||
// BadRequest sends a 400 error to the browser.
|
||||
func BadRequest(w http.ResponseWriter, err error) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
if err != nil {
|
||||
logger.Error("[Bad Request] %v", err)
|
||||
w.Write([]byte("Bad Request: " + err.Error()))
|
||||
} else {
|
||||
w.Write([]byte("Bad Request"))
|
||||
}
|
||||
}
|
||||
|
||||
// NotFound sends a 404 error to the browser.
|
||||
func NotFound(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("Page Not Found"))
|
||||
}
|
||||
|
||||
// Forbidden sends a 403 error to the browser.
|
||||
func Forbidden(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("Access Forbidden"))
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
)
|
||||
|
||||
// OK sends a JSON response with the status code 200.
|
||||
func OK(w http.ResponseWriter, v interface{}) {
|
||||
commonHeaders(w)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(toJSON(v))
|
||||
}
|
||||
|
||||
// Created sends a JSON response with the status code 201.
|
||||
func Created(w http.ResponseWriter, v interface{}) {
|
||||
commonHeaders(w)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write(toJSON(v))
|
||||
}
|
||||
|
||||
// NoContent sends a JSON response with the status code 204.
|
||||
func NoContent(w http.ResponseWriter) {
|
||||
commonHeaders(w)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// NotFound sends a JSON response with the status code 404.
|
||||
func NotFound(w http.ResponseWriter, err error) {
|
||||
logger.Error("[Not Found] %v", err)
|
||||
commonHeaders(w)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write(encodeError(err))
|
||||
}
|
||||
|
||||
// ServerError sends a JSON response with the status code 500.
|
||||
func ServerError(w http.ResponseWriter, err error) {
|
||||
logger.Error("[Internal Server Error] %v", err)
|
||||
commonHeaders(w)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
if err != nil {
|
||||
w.Write(encodeError(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Forbidden sends a JSON response with the status code 403.
|
||||
func Forbidden(w http.ResponseWriter) {
|
||||
logger.Info("[Forbidden]")
|
||||
commonHeaders(w)
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write(encodeError(errors.New("Access Forbidden")))
|
||||
}
|
||||
|
||||
// Unauthorized sends a JSON response with the status code 401.
|
||||
func Unauthorized(w http.ResponseWriter) {
|
||||
commonHeaders(w)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write(encodeError(errors.New("Access Unauthorized")))
|
||||
}
|
||||
|
||||
// BadRequest sends a JSON response with the status code 400.
|
||||
func BadRequest(w http.ResponseWriter, err error) {
|
||||
logger.Error("[Bad Request] %v", err)
|
||||
commonHeaders(w)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
if err != nil {
|
||||
w.Write(encodeError(err))
|
||||
}
|
||||
}
|
||||
|
||||
func commonHeaders(w http.ResponseWriter) {
|
||||
w.Header().Set("Accept", "application/json")
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
}
|
||||
|
||||
func encodeError(err error) []byte {
|
||||
type errorMsg struct {
|
||||
ErrorMessage string `json:"error_message"`
|
||||
}
|
||||
|
||||
tmp := errorMsg{ErrorMessage: err.Error()}
|
||||
data, err := json.Marshal(tmp)
|
||||
if err != nil {
|
||||
logger.Error("json encoding error: %v", err)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func toJSON(v interface{}) []byte {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
logger.Error("json encoding error: %v", err)
|
||||
return []byte("")
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package response
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Redirect redirects the user to another location.
|
||||
func Redirect(w http.ResponseWriter, r *http.Request, path string) {
|
||||
http.Redirect(w, r, path, http.StatusFound)
|
||||
}
|
||||
|
||||
// NotModified sends a response with a 304 status code.
|
||||
func NotModified(w http.ResponseWriter) {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
}
|
||||
|
||||
// Cache returns a response with caching headers.
|
||||
func Cache(w http.ResponseWriter, r *http.Request, mimeType, etag string, content []byte, duration time.Duration) {
|
||||
w.Header().Set("Content-Type", mimeType)
|
||||
w.Header().Set("ETag", etag)
|
||||
w.Header().Set("Cache-Control", "public")
|
||||
w.Header().Set("Expires", time.Now().Add(duration).Format(time.RFC1123))
|
||||
|
||||
if etag == r.Header.Get("If-None-Match") {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
} else {
|
||||
w.Write(content)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package xml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// OK sends a XML document.
|
||||
func OK(w http.ResponseWriter, data string) {
|
||||
w.Header().Set("Content-Type", "text/xml")
|
||||
w.Write([]byte(data))
|
||||
}
|
||||
|
||||
// Attachment forces the download of a XML document.
|
||||
func Attachment(w http.ResponseWriter, filename, data string) {
|
||||
w.Header().Set("Content-Type", "text/xml")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
||||
w.Write([]byte(data))
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/miniflux/miniflux/http"
|
||||
"github.com/miniflux/miniflux/http/client"
|
||||
)
|
||||
|
||||
// Client represents an Instapaper client.
|
||||
@@ -19,18 +19,27 @@ type Client struct {
|
||||
|
||||
// AddURL sends a link to Instapaper.
|
||||
func (c *Client) AddURL(link, title string) error {
|
||||
if c.username == "" || c.password == "" {
|
||||
return fmt.Errorf("instapaper: missing credentials")
|
||||
}
|
||||
|
||||
values := url.Values{}
|
||||
values.Add("url", link)
|
||||
values.Add("title", title)
|
||||
|
||||
apiURL := "https://www.instapaper.com/api/add?" + values.Encode()
|
||||
client := http.NewClientWithCredentials(apiURL, c.username, c.password)
|
||||
response, err := client.Get()
|
||||
clt := client.New(apiURL)
|
||||
clt.WithCredentials(c.username, c.password)
|
||||
response, err := clt.Get()
|
||||
if err != nil {
|
||||
return fmt.Errorf("instapaper: unable to send url: %v", err)
|
||||
}
|
||||
|
||||
if response.HasServerFailure() {
|
||||
return fmt.Errorf("instapaper: unable to send url, status=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewClient returns a new Instapaper client.
|
||||
|
||||
@@ -5,16 +5,18 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"github.com/miniflux/miniflux/config"
|
||||
"github.com/miniflux/miniflux/integration/instapaper"
|
||||
"github.com/miniflux/miniflux/integration/nunuxkeeper"
|
||||
"github.com/miniflux/miniflux/integration/pinboard"
|
||||
"github.com/miniflux/miniflux/integration/pocket"
|
||||
"github.com/miniflux/miniflux/integration/wallabag"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
)
|
||||
|
||||
// SendEntry send the entry to the activated providers.
|
||||
func SendEntry(entry *model.Entry, integration *model.Integration) {
|
||||
func SendEntry(cfg *config.Config, entry *model.Entry, integration *model.Integration) {
|
||||
if integration.PinboardEnabled {
|
||||
client := pinboard.NewClient(integration.PinboardToken)
|
||||
err := client.AddBookmark(
|
||||
@@ -25,14 +27,14 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Error("[Integration] %v", err)
|
||||
logger.Error("[Integration] UserID #%d: %v", integration.UserID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if integration.InstapaperEnabled {
|
||||
client := instapaper.NewClient(integration.InstapaperUsername, integration.InstapaperPassword)
|
||||
if err := client.AddURL(entry.URL, entry.Title); err != nil {
|
||||
logger.Error("[Integration] %v", err)
|
||||
logger.Error("[Integration] UserID #%d: %v", integration.UserID, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +48,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
|
||||
)
|
||||
|
||||
if err := client.AddEntry(entry.URL, entry.Title); err != nil {
|
||||
logger.Error("[Integration] %v", err)
|
||||
logger.Error("[Integration] UserID #%d: %v", integration.UserID, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +59,14 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
|
||||
)
|
||||
|
||||
if err := client.AddEntry(entry.URL, entry.Title, entry.Content); err != nil {
|
||||
logger.Error("[Integration] %v", err)
|
||||
logger.Error("[Integration] UserID #%d: %v", integration.UserID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if integration.PocketEnabled {
|
||||
client := pocket.NewClient(cfg.PocketConsumerKey(integration.PocketConsumerKey), integration.PocketAccessToken)
|
||||
if err := client.AddURL(entry.URL, entry.Title); err != nil {
|
||||
logger.Error("[Integration] UserID #%d: %v", integration.UserID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/miniflux/miniflux/http"
|
||||
"github.com/miniflux/miniflux/http/client"
|
||||
)
|
||||
|
||||
// Document structure of a Nununx Keeper document
|
||||
@@ -28,6 +28,10 @@ type Client struct {
|
||||
|
||||
// AddEntry sends an entry to Nunux Keeper.
|
||||
func (c *Client) AddEntry(link, title, content string) error {
|
||||
if c.baseURL == "" || c.apiKey == "" {
|
||||
return fmt.Errorf("nunux-keeper: missing credentials")
|
||||
}
|
||||
|
||||
doc := &Document{
|
||||
Title: title,
|
||||
Origin: link,
|
||||
@@ -39,13 +43,19 @@ func (c *Client) AddEntry(link, title, content string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := http.NewClientWithCredentials(apiURL, "api", c.apiKey)
|
||||
response, err := client.PostJSON(doc)
|
||||
|
||||
clt := client.New(apiURL)
|
||||
clt.WithCredentials("api", c.apiKey)
|
||||
response, err := clt.PostJSON(doc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("nunux-keeper: unable to send entry: %v", err)
|
||||
}
|
||||
|
||||
if response.HasServerFailure() {
|
||||
return fmt.Errorf("nunux-keeper: unable to send entry, status=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewClient returns a new Nunux Keeepr client.
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/miniflux/miniflux/http"
|
||||
"github.com/miniflux/miniflux/http/client"
|
||||
)
|
||||
|
||||
// Client represents a Pinboard client.
|
||||
@@ -18,6 +18,10 @@ type Client struct {
|
||||
|
||||
// AddBookmark sends a link to Pinboard.
|
||||
func (c *Client) AddBookmark(link, title, tags string, markAsUnread bool) error {
|
||||
if c.authToken == "" {
|
||||
return fmt.Errorf("pinboard: missing credentials")
|
||||
}
|
||||
|
||||
toRead := "no"
|
||||
if markAsUnread {
|
||||
toRead = "yes"
|
||||
@@ -30,13 +34,17 @@ func (c *Client) AddBookmark(link, title, tags string, markAsUnread bool) error
|
||||
values.Add("tags", tags)
|
||||
values.Add("toread", toRead)
|
||||
|
||||
client := http.NewClient("https://api.pinboard.in/v1/posts/add?" + values.Encode())
|
||||
response, err := client.Get()
|
||||
clt := client.New("https://api.pinboard.in/v1/posts/add?" + values.Encode())
|
||||
response, err := clt.Get()
|
||||
if err != nil {
|
||||
return fmt.Errorf("pinboard: unable to send bookmark: %v", err)
|
||||
}
|
||||
|
||||
if response.HasServerFailure() {
|
||||
return fmt.Errorf("pinboard: unable to send bookmark, status=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewClient returns a new Pinboard client.
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package pocket
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
|
||||
"github.com/miniflux/miniflux/http/client"
|
||||
)
|
||||
|
||||
// Connector manages the authorization flow with Pocket to get a personal access token.
|
||||
type Connector struct {
|
||||
consumerKey string
|
||||
}
|
||||
|
||||
// RequestToken fetches a new request token from Pocket API.
|
||||
func (c *Connector) RequestToken(redirectURL string) (string, error) {
|
||||
type req struct {
|
||||
ConsumerKey string `json:"consumer_key"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
clt := client.New("https://getpocket.com/v3/oauth/request")
|
||||
response, err := clt.PostJSON(&req{ConsumerKey: c.consumerKey, RedirectURI: redirectURL})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pocket: unable to fetch request token: %v", err)
|
||||
}
|
||||
|
||||
if response.HasServerFailure() {
|
||||
return "", fmt.Errorf("pocket: unable to fetch request token, status=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pocket: unable to read response body: %v", err)
|
||||
}
|
||||
|
||||
values, err := url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pocket: unable to parse response: %v", err)
|
||||
}
|
||||
|
||||
code := values.Get("code")
|
||||
if code == "" {
|
||||
return "", errors.New("pocket: code is empty")
|
||||
}
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
||||
// AccessToken fetches a new access token once the end-user authorized the application.
|
||||
func (c *Connector) AccessToken(requestToken string) (string, error) {
|
||||
type req struct {
|
||||
ConsumerKey string `json:"consumer_key"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
clt := client.New("https://getpocket.com/v3/oauth/authorize")
|
||||
response, err := clt.PostJSON(&req{ConsumerKey: c.consumerKey, Code: requestToken})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pocket: unable to fetch access token: %v", err)
|
||||
}
|
||||
|
||||
if response.HasServerFailure() {
|
||||
return "", fmt.Errorf("pocket: unable to fetch access token, status=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pocket: unable to read response body: %v", err)
|
||||
}
|
||||
|
||||
values, err := url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pocket: unable to parse response: %v", err)
|
||||
}
|
||||
|
||||
token := values.Get("access_token")
|
||||
if token == "" {
|
||||
return "", errors.New("pocket: access_token is empty")
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// AuthorizationURL returns the authorization URL for the end-user.
|
||||
func (c *Connector) AuthorizationURL(requestToken, redirectURL string) string {
|
||||
return fmt.Sprintf(
|
||||
"https://getpocket.com/auth/authorize?request_token=%s&redirect_uri=%s",
|
||||
requestToken,
|
||||
redirectURL,
|
||||
)
|
||||
}
|
||||
|
||||
// NewConnector returns a new Pocket Connector.
|
||||
func NewConnector(consumerKey string) *Connector {
|
||||
return &Connector{consumerKey}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package pocket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/miniflux/miniflux/http/client"
|
||||
)
|
||||
|
||||
// Client represents a Pocket client.
|
||||
type Client struct {
|
||||
consumerKey string
|
||||
accessToken string
|
||||
}
|
||||
|
||||
// AddURL sends a single link to Pocket.
|
||||
func (c *Client) AddURL(link, title string) error {
|
||||
if c.consumerKey == "" || c.accessToken == "" {
|
||||
return fmt.Errorf("pocket: missing credentials")
|
||||
}
|
||||
|
||||
type body struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ConsumerKey string `json:"consumer_key"`
|
||||
Title string `json:"title,omitempty"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
data := &body{
|
||||
AccessToken: c.accessToken,
|
||||
ConsumerKey: c.consumerKey,
|
||||
Title: title,
|
||||
URL: link,
|
||||
}
|
||||
|
||||
clt := client.New("https://getpocket.com/v3/add")
|
||||
response, err := clt.PostJSON(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pocket: unable to send url: %v", err)
|
||||
}
|
||||
|
||||
if response.HasServerFailure() {
|
||||
return fmt.Errorf("pocket: unable to send url, status=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewClient returns a new Pocket client.
|
||||
func NewClient(consumerKey, accessToken string) *Client {
|
||||
return &Client{consumerKey, accessToken}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
"github.com/miniflux/miniflux/http"
|
||||
"github.com/miniflux/miniflux/http/client"
|
||||
)
|
||||
|
||||
// Client represents a Wallabag client.
|
||||
@@ -24,6 +24,10 @@ type Client struct {
|
||||
|
||||
// AddEntry sends a link to Wallabag.
|
||||
func (c *Client) AddEntry(link, title string) error {
|
||||
if c.baseURL == "" || c.clientID == "" || c.clientSecret == "" || c.username == "" || c.password == "" {
|
||||
return fmt.Errorf("wallabag: missing credentials")
|
||||
}
|
||||
|
||||
accessToken, err := c.getAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -38,8 +42,9 @@ func (c *Client) createEntry(accessToken, link, title string) error {
|
||||
return fmt.Errorf("wallbag: unable to get entries endpoint: %v", err)
|
||||
}
|
||||
|
||||
client := http.NewClientWithAuthorization(endpoint, "Bearer "+accessToken)
|
||||
response, err := client.PostJSON(map[string]string{"url": link, "title": title})
|
||||
clt := client.New(endpoint)
|
||||
clt.WithAuthorization("Bearer " + accessToken)
|
||||
response, err := clt.PostJSON(map[string]string{"url": link, "title": title})
|
||||
if err != nil {
|
||||
return fmt.Errorf("wallabag: unable to post entry: %v", err)
|
||||
}
|
||||
@@ -64,8 +69,8 @@ func (c *Client) getAccessToken() (string, error) {
|
||||
return "", fmt.Errorf("wallbag: unable to get token endpoint: %v", err)
|
||||
}
|
||||
|
||||
client := http.NewClient(endpoint)
|
||||
response, err := client.PostForm(values)
|
||||
clt := client.New(endpoint)
|
||||
response, err := clt.PostForm(values)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("wallabag: unable to get access token: %v", err)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -42,6 +44,22 @@ func TestWithWrongCredentials(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCurrentLoggedUser(t *testing.T) {
|
||||
client := miniflux.NewClient(testBaseURL, testAdminUsername, testAdminPassword)
|
||||
user, err := client.Me()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if user.ID == 0 {
|
||||
t.Fatalf(`Invalid userID, got %q`, user.ID)
|
||||
}
|
||||
|
||||
if user.Username != testAdminUsername {
|
||||
t.Fatalf(`Invalid username, got %q`, user.Username)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUsers(t *testing.T) {
|
||||
client := miniflux.NewClient(testBaseURL, testAdminUsername, testAdminPassword)
|
||||
users, err := client.Users()
|
||||
@@ -653,6 +671,32 @@ func TestExport(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImport(t *testing.T) {
|
||||
username := getRandomUsername()
|
||||
client := miniflux.NewClient(testBaseURL, testAdminUsername, testAdminPassword)
|
||||
_, err := client.CreateUser(username, testStandardPassword, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
client = miniflux.NewClient(testBaseURL, username, testStandardPassword)
|
||||
|
||||
data := `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<opml version="2.0">
|
||||
<body>
|
||||
<outline text="Test Category">
|
||||
<outline title="Test" text="Test" xmlUrl="` + testFeedURL + `" htmlUrl="` + testWebsiteURL + `"></outline>
|
||||
</outline>
|
||||
</body>
|
||||
</opml>`
|
||||
|
||||
b := bytes.NewReader([]byte(data))
|
||||
err = client.Import(ioutil.NopCloser(b))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeed(t *testing.T) {
|
||||
username := getRandomUsername()
|
||||
client := miniflux.NewClient(testBaseURL, testAdminUsername, testAdminPassword)
|
||||
|
||||
+22
-12
@@ -1,5 +1,5 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2018-04-09 20:38:50.319066697 -0700 PDT m=+0.026775461
|
||||
// 2018-05-20 15:22:33.795349513 -0700 PDT m=+0.026820026
|
||||
|
||||
package locale
|
||||
|
||||
@@ -161,7 +161,7 @@ var translations = map[string]string{
|
||||
"Scraper Rules": "Extraktionsregeln",
|
||||
"Rewrite Rules": "Umschreiberegeln",
|
||||
"Preferences saved!": "Einstellungen gespeichert!",
|
||||
"Your external account is now linked !": "Ihr externes Konto wurde verlinkt!",
|
||||
"Your external account is now linked!": "Ihr externes Konto wurde verlinkt!",
|
||||
"Save articles to Wallabag": "Artikel in Wallabag speichern",
|
||||
"Wallabag API Endpoint": "Wallabag URL",
|
||||
"Wallabag Client ID": "Wallabag Client-ID",
|
||||
@@ -395,7 +395,7 @@ var translations = map[string]string{
|
||||
"Scraper Rules": "Règles pour récupérer le contenu original",
|
||||
"Rewrite Rules": "Règles de réécriture",
|
||||
"Preferences saved!": "Préférences sauvegardées !",
|
||||
"Your external account is now linked !": "Votre compte externe est maintenant associé !",
|
||||
"Your external account is now linked!": "Votre compte externe est maintenant associé !",
|
||||
"Save articles to Wallabag": "Sauvegarder les articles vers Wallabag",
|
||||
"Wallabag API Endpoint": "URL de l'API de Wallabag",
|
||||
"Wallabag Client ID": "Identifiant du client Wallabag",
|
||||
@@ -459,7 +459,17 @@ var translations = map[string]string{
|
||||
"This website is permanently unreachable (original error: %q)": "Ce site web n'est pas joignable de façon permanente (erreur originale : %q)",
|
||||
"Website unreachable, the request timed out after %d seconds": "Site web injoignable, la requête à échouée après %d secondes",
|
||||
"Comments": "Commentaires",
|
||||
"View Comments": "Voir les commentaires"
|
||||
"View Comments": "Voir les commentaires",
|
||||
"This file is empty": "Ce fichier est vide",
|
||||
"Your external account is now dissociated!": "Votre compte externe est maintenant dissocié !",
|
||||
"You must define a password otherwise you won't be able to login again.": "Vous devez définir un mot de passe sinon vous ne pourrez plus vous connecter par la suite.",
|
||||
"Save articles to Pocket": "Sauvegarder les articles vers Pocket",
|
||||
"Connect your Pocket account": "Connectez votre compte Pocket",
|
||||
"Pocket Consumer Key": "« Pocket Consumer Key »",
|
||||
"Pocket Access Token": "« Pocket Access Token »",
|
||||
"Your Pocket account is now linked!": "Votre compte Pocket est maintenant connecté !",
|
||||
"Unable to fetch access token from Pocket!": "Impossible de récupérer le jeton d'accès depuis Pocket !",
|
||||
"Unable to fetch request token from Pocket!": "Impossible de récupérer le jeton d'accès depuis Pocket !"
|
||||
}
|
||||
`,
|
||||
"nl_NL": `{
|
||||
@@ -618,7 +628,7 @@ var translations = map[string]string{
|
||||
"Scraper Rules": "Scraper regels",
|
||||
"Rewrite Rules": "Rewrite regels",
|
||||
"Preferences saved!": "Instellingen opgeslagen!",
|
||||
"Your external account is now linked !": "Jouw externe account is nu gekoppeld!",
|
||||
"Your external account is now linked!": "Jouw externe account is nu gekoppeld!",
|
||||
"Save articles to Wallabag": "Sauvegarder les articles vers Wallabag",
|
||||
"Wallabag API Endpoint": "Wallabag URL",
|
||||
"Wallabag Client ID": "Wallabag Client-ID",
|
||||
@@ -842,7 +852,7 @@ var translations = map[string]string{
|
||||
"Scraper Rules": "Zasady ekstrakcji",
|
||||
"Rewrite Rules": "Reguły zapisu",
|
||||
"Preferences saved!": "Ustawienia zapisane!",
|
||||
"Your external account is now linked !": "Twoje zewnętrzne konto jest teraz połączone!",
|
||||
"Your external account is now linked!": "Twoje zewnętrzne konto jest teraz połączone!",
|
||||
"Save articles to Wallabag": "Zapisz artykuły do Wallabag",
|
||||
"Wallabag API Endpoint": "Wallabag URL",
|
||||
"Wallabag Client ID": "Wallabag Client-ID",
|
||||
@@ -1064,7 +1074,7 @@ var translations = map[string]string{
|
||||
"Scraper Rules": "Scraper规则",
|
||||
"Rewrite Rules": "重写规则",
|
||||
"Preferences saved!": "偏好已存储!",
|
||||
"Your external account is now linked !": "您的外部账号已关联!",
|
||||
"Your external account is now linked!": "您的外部账号已关联!",
|
||||
"Save articles to Wallabag": "保存文章到Wallabag",
|
||||
"Wallabag API Endpoint": "Wallabag URL",
|
||||
"Wallabag Client ID": "Wallabag 客户端ID",
|
||||
@@ -1132,10 +1142,10 @@ var translations = map[string]string{
|
||||
}
|
||||
|
||||
var translationsChecksums = map[string]string{
|
||||
"de_DE": "df47fc009e6a021579c7e004ebc0b00eae7bf47c23daaf74489fb4c15881296f",
|
||||
"de_DE": "791d72c96137ab03b729017bdfa27c8eed2f65912e372fcb5b2796d5099d5498",
|
||||
"en_US": "6fe95384260941e8a5a3c695a655a932e0a8a6a572c1e45cb2b1ae8baa01b897",
|
||||
"fr_FR": "fb572aee29b90fcaa866e97d2e1cab8c6b8cd8a1cb76c7e83d7aa778748dd283",
|
||||
"nl_NL": "d427d6a5e843be576040dee004df2b685a839a38b2e5f06435faa2973f1f4c70",
|
||||
"pl_PL": "4dcf7c3f44c80ca81ecdbef96bdb21d1ae1a8a6caf60cc11403e5e041efc5ca9",
|
||||
"zh_CN": "bfa05d3b3396df6222414a3a6949b73b486cd021499ecd3a34ce8e04e93aad93",
|
||||
"fr_FR": "808f1561135bb3d0f886833f00a51402113925e9086c01f71b30bcc679d9b7c2",
|
||||
"nl_NL": "1a73f1dd1c4c0d2c2adc8695cdd050c2dad81c14876caed3892b44adc2491265",
|
||||
"pl_PL": "da709c14ff71f3b516eec66cb2758d89c5feab1472c94b2b518f425162a9f806",
|
||||
"zh_CN": "d80594c1b67d15e9f4673d3d62fe4949e8606a5fdfb741d8a8921f21dceb8cf2",
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
"Scraper Rules": "Extraktionsregeln",
|
||||
"Rewrite Rules": "Umschreiberegeln",
|
||||
"Preferences saved!": "Einstellungen gespeichert!",
|
||||
"Your external account is now linked !": "Ihr externes Konto wurde verlinkt!",
|
||||
"Your external account is now linked!": "Ihr externes Konto wurde verlinkt!",
|
||||
"Save articles to Wallabag": "Artikel in Wallabag speichern",
|
||||
"Wallabag API Endpoint": "Wallabag URL",
|
||||
"Wallabag Client ID": "Wallabag Client-ID",
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
"Scraper Rules": "Règles pour récupérer le contenu original",
|
||||
"Rewrite Rules": "Règles de réécriture",
|
||||
"Preferences saved!": "Préférences sauvegardées !",
|
||||
"Your external account is now linked !": "Votre compte externe est maintenant associé !",
|
||||
"Your external account is now linked!": "Votre compte externe est maintenant associé !",
|
||||
"Save articles to Wallabag": "Sauvegarder les articles vers Wallabag",
|
||||
"Wallabag API Endpoint": "URL de l'API de Wallabag",
|
||||
"Wallabag Client ID": "Identifiant du client Wallabag",
|
||||
@@ -219,5 +219,15 @@
|
||||
"This website is permanently unreachable (original error: %q)": "Ce site web n'est pas joignable de façon permanente (erreur originale : %q)",
|
||||
"Website unreachable, the request timed out after %d seconds": "Site web injoignable, la requête à échouée après %d secondes",
|
||||
"Comments": "Commentaires",
|
||||
"View Comments": "Voir les commentaires"
|
||||
"View Comments": "Voir les commentaires",
|
||||
"This file is empty": "Ce fichier est vide",
|
||||
"Your external account is now dissociated!": "Votre compte externe est maintenant dissocié !",
|
||||
"You must define a password otherwise you won't be able to login again.": "Vous devez définir un mot de passe sinon vous ne pourrez plus vous connecter par la suite.",
|
||||
"Save articles to Pocket": "Sauvegarder les articles vers Pocket",
|
||||
"Connect your Pocket account": "Connectez votre compte Pocket",
|
||||
"Pocket Consumer Key": "« Pocket Consumer Key »",
|
||||
"Pocket Access Token": "« Pocket Access Token »",
|
||||
"Your Pocket account is now linked!": "Votre compte Pocket est maintenant connecté !",
|
||||
"Unable to fetch access token from Pocket!": "Impossible de récupérer le jeton d'accès depuis Pocket !",
|
||||
"Unable to fetch request token from Pocket!": "Impossible de récupérer le jeton d'accès depuis Pocket !"
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
"Scraper Rules": "Scraper regels",
|
||||
"Rewrite Rules": "Rewrite regels",
|
||||
"Preferences saved!": "Instellingen opgeslagen!",
|
||||
"Your external account is now linked !": "Jouw externe account is nu gekoppeld!",
|
||||
"Your external account is now linked!": "Jouw externe account is nu gekoppeld!",
|
||||
"Save articles to Wallabag": "Sauvegarder les articles vers Wallabag",
|
||||
"Wallabag API Endpoint": "Wallabag URL",
|
||||
"Wallabag Client ID": "Wallabag Client-ID",
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
"Scraper Rules": "Zasady ekstrakcji",
|
||||
"Rewrite Rules": "Reguły zapisu",
|
||||
"Preferences saved!": "Ustawienia zapisane!",
|
||||
"Your external account is now linked !": "Twoje zewnętrzne konto jest teraz połączone!",
|
||||
"Your external account is now linked!": "Twoje zewnętrzne konto jest teraz połączone!",
|
||||
"Save articles to Wallabag": "Zapisz artykuły do Wallabag",
|
||||
"Wallabag API Endpoint": "Wallabag URL",
|
||||
"Wallabag Client ID": "Wallabag Client-ID",
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
"Scraper Rules": "Scraper规则",
|
||||
"Rewrite Rules": "重写规则",
|
||||
"Preferences saved!": "偏好已存储!",
|
||||
"Your external account is now linked !": "您的外部账号已关联!",
|
||||
"Your external account is now linked!": "您的外部账号已关联!",
|
||||
"Save articles to Wallabag": "保存文章到Wallabag",
|
||||
"Wallabag API Endpoint": "Wallabag URL",
|
||||
"Wallabag Client ID": "Wallabag 客户端ID",
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/cookie"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
)
|
||||
|
||||
// AppSession handles application session middleware.
|
||||
func (m *Middleware) AppSession(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
session := m.getAppSessionValueFromCookie(r)
|
||||
|
||||
if session == nil {
|
||||
logger.Debug("[Middleware:AppSession] Session not found")
|
||||
|
||||
session, err = m.store.CreateSession()
|
||||
if err != nil {
|
||||
logger.Error("[Middleware:AppSession] %v", err)
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie.New(cookie.CookieSessionID, session.ID, m.cfg.IsHTTPS, m.cfg.BasePath()))
|
||||
} else {
|
||||
logger.Debug("[Middleware:AppSession] %s", session)
|
||||
}
|
||||
|
||||
if r.Method == "POST" {
|
||||
formValue := r.FormValue("csrf")
|
||||
headerValue := r.Header.Get("X-Csrf-Token")
|
||||
|
||||
if session.Data.CSRF != formValue && session.Data.CSRF != headerValue {
|
||||
logger.Error(`[Middleware:AppSession] Invalid or missing CSRF token: Form="%s", Header="%s"`, formValue, headerValue)
|
||||
html.BadRequest(w, errors.New("invalid or missing CSRF"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, SessionIDContextKey, session.ID)
|
||||
ctx = context.WithValue(ctx, CSRFContextKey, session.Data.CSRF)
|
||||
ctx = context.WithValue(ctx, OAuth2StateContextKey, session.Data.OAuth2State)
|
||||
ctx = context.WithValue(ctx, FlashMessageContextKey, session.Data.FlashMessage)
|
||||
ctx = context.WithValue(ctx, FlashErrorMessageContextKey, session.Data.FlashErrorMessage)
|
||||
ctx = context.WithValue(ctx, UserLanguageContextKey, session.Data.Language)
|
||||
ctx = context.WithValue(ctx, PocketRequestTokenContextKey, session.Data.PocketRequestToken)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Middleware) getAppSessionValueFromCookie(r *http.Request) *model.Session {
|
||||
cookieValue := request.Cookie(r, cookie.CookieSessionID)
|
||||
if cookieValue == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
session, err := m.store.Session(cookieValue)
|
||||
if err != nil {
|
||||
logger.Error("[Middleware:AppSession] %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
@@ -8,53 +8,43 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/response/json"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
)
|
||||
|
||||
// BasicAuthMiddleware is the middleware for HTTP Basic authentication.
|
||||
type BasicAuthMiddleware struct {
|
||||
store *storage.Storage
|
||||
}
|
||||
|
||||
// Handler executes the middleware.
|
||||
func (b *BasicAuthMiddleware) Handler(next http.Handler) http.Handler {
|
||||
// BasicAuth handles HTTP basic authentication.
|
||||
func (m *Middleware) BasicAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
errorResponse := `{"error_message": "Not Authorized"}`
|
||||
|
||||
username, password, authOK := r.BasicAuth()
|
||||
if !authOK {
|
||||
logger.Debug("[Middleware:BasicAuth] No authentication headers sent")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(errorResponse))
|
||||
json.Unauthorized(w)
|
||||
return
|
||||
}
|
||||
|
||||
if err := b.store.CheckPassword(username, password); err != nil {
|
||||
if err := m.store.CheckPassword(username, password); err != nil {
|
||||
logger.Info("[Middleware:BasicAuth] Invalid username or password: %s", username)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(errorResponse))
|
||||
json.Unauthorized(w)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := b.store.UserByUsername(username)
|
||||
user, err := m.store.UserByUsername(username)
|
||||
if err != nil {
|
||||
logger.Error("[Middleware:BasicAuth] %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(errorResponse))
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
logger.Info("[Middleware:BasicAuth] User not found: %s", username)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(errorResponse))
|
||||
json.Unauthorized(w)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("[Middleware:BasicAuth] User authenticated: %s", username)
|
||||
b.store.SetLastLogin(user.ID)
|
||||
m.store.SetLastLogin(user.ID)
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, UserIDContextKey, user.ID)
|
||||
@@ -65,8 +55,3 @@ func (b *BasicAuthMiddleware) Handler(next http.Handler) http.Handler {
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// NewBasicAuthMiddleware returns a new BasicAuthMiddleware.
|
||||
func NewBasicAuthMiddleware(s *storage.Storage) *BasicAuthMiddleware {
|
||||
return &BasicAuthMiddleware{store: s}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// CommonHeaders sends common HTTP headers.
|
||||
func (m *Middleware) CommonHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'self'; img-src *; media-src *; frame-src *; child-src *")
|
||||
|
||||
if m.cfg.IsHTTPS && m.cfg.HasHSTS() {
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31536000")
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
@@ -46,4 +46,7 @@ var (
|
||||
|
||||
// FlashErrorMessageContextKey is the context key used to store a flash error message.
|
||||
FlashErrorMessageContextKey = &ContextKey{"FlashErrorMessage"}
|
||||
|
||||
// PocketRequestTokenContextKey is the context key for Pocket Request Token.
|
||||
PocketRequestTokenContextKey = &ContextKey{"PocketRequestToken"}
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
@@ -8,38 +8,30 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/response/json"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
)
|
||||
|
||||
// FeverMiddleware is the middleware that handles Fever API.
|
||||
type FeverMiddleware struct {
|
||||
store *storage.Storage
|
||||
}
|
||||
|
||||
// Handler executes the middleware.
|
||||
func (f *FeverMiddleware) Handler(next http.Handler) http.Handler {
|
||||
// FeverAuth handles Fever API authentication.
|
||||
func (m *Middleware) FeverAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
logger.Debug("[Middleware:Fever]")
|
||||
|
||||
apiKey := r.FormValue("api_key")
|
||||
user, err := f.store.UserByFeverToken(apiKey)
|
||||
|
||||
user, err := m.store.UserByFeverToken(apiKey)
|
||||
if err != nil {
|
||||
logger.Error("[Fever] %v", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"api_version": 3, "auth": 0}`))
|
||||
logger.Error("[Middleware:Fever] %v", err)
|
||||
json.OK(w, map[string]int{"api_version": 3, "auth": 0})
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
logger.Info("[Middleware:Fever] Fever authentication failure")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"api_version": 3, "auth": 0}`))
|
||||
json.OK(w, map[string]int{"api_version": 3, "auth": 0})
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("[Middleware:Fever] User #%d is authenticated", user.ID)
|
||||
f.store.SetLastLogin(user.ID)
|
||||
m.store.SetLastLogin(user.ID)
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, UserIDContextKey, user.ID)
|
||||
@@ -50,8 +42,3 @@ func (f *FeverMiddleware) Handler(next http.Handler) http.Handler {
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// NewFeverMiddleware returns a new FeverMiddleware.
|
||||
func NewFeverMiddleware(s *storage.Storage) *FeverMiddleware {
|
||||
return &FeverMiddleware{store: s}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HeaderConfig changes config values according to HTTP headers.
|
||||
func (m *Middleware) HeaderConfig(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
m.cfg.IsHTTPS = true
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
)
|
||||
|
||||
// Logging logs the HTTP request.
|
||||
func (m *Middleware) Logging(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
logger.Debug("[HTTP] %s %s %s", request.RealIP(r), r.Method, r.RequestURI)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/miniflux/miniflux/config"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
)
|
||||
|
||||
// Middleware handles different middleware handlers.
|
||||
type Middleware struct {
|
||||
cfg *config.Config
|
||||
store *storage.Storage
|
||||
router *mux.Router
|
||||
}
|
||||
|
||||
// New returns a new middleware.
|
||||
func New(cfg *config.Config, store *storage.Storage, router *mux.Router) *Middleware {
|
||||
return &Middleware{cfg, store, router}
|
||||
}
|
||||
@@ -9,34 +9,30 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/cookie"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// UserSessionMiddleware represents a user session middleware.
|
||||
type UserSessionMiddleware struct {
|
||||
store *storage.Storage
|
||||
router *mux.Router
|
||||
}
|
||||
|
||||
// Handler execute the middleware.
|
||||
func (s *UserSessionMiddleware) Handler(next http.Handler) http.Handler {
|
||||
// UserSession handles the user session middleware.
|
||||
func (m *Middleware) UserSession(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
session := s.getSessionFromCookie(r)
|
||||
session := m.getUserSessionFromCookie(r)
|
||||
|
||||
if session == nil {
|
||||
logger.Debug("[Middleware:UserSession] Session not found")
|
||||
if s.isPublicRoute(r) {
|
||||
if m.isPublicRoute(r) {
|
||||
next.ServeHTTP(w, r)
|
||||
} else {
|
||||
http.Redirect(w, r, route.Path(s.router, "login"), http.StatusFound)
|
||||
response.Redirect(w, r, route.Path(m.router, "login"))
|
||||
}
|
||||
} else {
|
||||
logger.Debug("[Middleware:UserSession] %s", session)
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, UserIDContextKey, session.UserID)
|
||||
ctx = context.WithValue(ctx, IsAuthenticatedContextKey, true)
|
||||
@@ -47,7 +43,7 @@ func (s *UserSessionMiddleware) Handler(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserSessionMiddleware) isPublicRoute(r *http.Request) bool {
|
||||
func (m *Middleware) isPublicRoute(r *http.Request) bool {
|
||||
route := mux.CurrentRoute(r)
|
||||
switch route.GetName() {
|
||||
case "login",
|
||||
@@ -65,13 +61,13 @@ func (s *UserSessionMiddleware) isPublicRoute(r *http.Request) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserSessionMiddleware) getSessionFromCookie(r *http.Request) *model.UserSession {
|
||||
sessionCookie, err := r.Cookie(cookie.CookieUserSessionID)
|
||||
if err == http.ErrNoCookie {
|
||||
func (m *Middleware) getUserSessionFromCookie(r *http.Request) *model.UserSession {
|
||||
cookieValue := request.Cookie(r, cookie.CookieUserSessionID)
|
||||
if cookieValue == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
session, err := s.store.UserSessionByToken(sessionCookie.Value)
|
||||
session, err := m.store.UserSessionByToken(cookieValue)
|
||||
if err != nil {
|
||||
logger.Error("[Middleware:UserSession] %v", err)
|
||||
return nil
|
||||
@@ -79,8 +75,3 @@ func (s *UserSessionMiddleware) getSessionFromCookie(r *http.Request) *model.Use
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
// NewUserSessionMiddleware returns a new UserSessionMiddleware.
|
||||
func NewUserSessionMiddleware(s *storage.Storage, r *mux.Router) *UserSessionMiddleware {
|
||||
return &UserSessionMiddleware{store: s, router: r}
|
||||
}
|
||||
@@ -27,4 +27,7 @@ type Integration struct {
|
||||
NunuxKeeperEnabled bool
|
||||
NunuxKeeperURL string
|
||||
NunuxKeeperAPIKey string
|
||||
PocketEnabled bool
|
||||
PocketAccessToken string
|
||||
PocketConsumerKey string
|
||||
}
|
||||
|
||||
+6
-5
@@ -13,11 +13,12 @@ import (
|
||||
|
||||
// SessionData represents the data attached to the session.
|
||||
type SessionData struct {
|
||||
CSRF string `json:"csrf"`
|
||||
OAuth2State string `json:"oauth2_state"`
|
||||
FlashMessage string `json:"flash_message"`
|
||||
FlashErrorMessage string `json:"flash_error_message"`
|
||||
Language string `json:"language"`
|
||||
CSRF string `json:"csrf"`
|
||||
OAuth2State string `json:"oauth2_state"`
|
||||
FlashMessage string `json:"flash_message"`
|
||||
FlashErrorMessage string `json:"flash_error_message"`
|
||||
Language string `json:"language"`
|
||||
PocketRequestToken string `json:"pocket_request_token"`
|
||||
}
|
||||
|
||||
func (s SessionData) String() string {
|
||||
|
||||
@@ -7,6 +7,7 @@ package date
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -194,6 +195,11 @@ var dateFormats = []string{
|
||||
// Parse parses a given date string using a large
|
||||
// list of commonly found feed date formats.
|
||||
func Parse(ds string) (t time.Time, err error) {
|
||||
timestamp, err := strconv.ParseInt(ds, 10, 64)
|
||||
if err == nil {
|
||||
return time.Unix(timestamp, 0), nil
|
||||
}
|
||||
|
||||
ds = replaceNonEnglishWords(ds)
|
||||
d := strings.TrimSpace(ds)
|
||||
if d == "" {
|
||||
|
||||
@@ -51,6 +51,7 @@ func TestParseWeirdDateFormat(t *testing.T) {
|
||||
"Di, 23 Jan 2018 00:00:00 +0100",
|
||||
"Do, 29 Mär 2018 00:00:00 +0200",
|
||||
"mer, 9 avr 2018 00:00:00 +0200",
|
||||
"1520932969",
|
||||
}
|
||||
|
||||
for _, date := range dates {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/errors"
|
||||
"github.com/miniflux/miniflux/http"
|
||||
"github.com/miniflux/miniflux/http/client"
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
@@ -43,8 +43,8 @@ func (h *Handler) CreateFeed(userID, categoryID int64, url string, crawler bool)
|
||||
return nil, errors.NewLocalizedError(errCategoryNotFound)
|
||||
}
|
||||
|
||||
client := http.NewClient(url)
|
||||
response, err := client.Get()
|
||||
clt := client.New(url)
|
||||
response, err := clt.Get()
|
||||
if err != nil {
|
||||
if _, ok := err.(*errors.LocalizedError); ok {
|
||||
return nil, err
|
||||
@@ -129,8 +129,9 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
|
||||
return errors.NewLocalizedError(errNotFound, feedID)
|
||||
}
|
||||
|
||||
client := http.NewClientWithCacheHeaders(originalFeed.FeedURL, originalFeed.EtagHeader, originalFeed.LastModifiedHeader)
|
||||
response, err := client.Get()
|
||||
clt := client.New(originalFeed.FeedURL)
|
||||
clt.WithCacheHeaders(originalFeed.EtagHeader, originalFeed.LastModifiedHeader)
|
||||
response, err := clt.Get()
|
||||
if err != nil {
|
||||
var customErr errors.LocalizedError
|
||||
if lerr, ok := err.(*errors.LocalizedError); ok {
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/miniflux/miniflux/crypto"
|
||||
"github.com/miniflux/miniflux/http"
|
||||
"github.com/miniflux/miniflux/http/client"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/url"
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
// FindIcon try to find the website's icon.
|
||||
func FindIcon(websiteURL string) (*model.Icon, error) {
|
||||
rootURL := url.RootURL(websiteURL)
|
||||
client := http.NewClient(rootURL)
|
||||
response, err := client.Get()
|
||||
clt := client.New(rootURL)
|
||||
response, err := clt.Get()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to download website index page: %v", err)
|
||||
}
|
||||
@@ -87,8 +87,8 @@ func parseDocument(websiteURL string, data io.Reader) (string, error) {
|
||||
}
|
||||
|
||||
func downloadIcon(iconURL string) (*model.Icon, error) {
|
||||
client := http.NewClient(iconURL)
|
||||
response, err := client.Get()
|
||||
clt := client.New(iconURL)
|
||||
response, err := clt.Get()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to download iconURL: %v", err)
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@ type Handler struct {
|
||||
func (h *Handler) Export(userID int64) (string, error) {
|
||||
feeds, err := h.store.Feeds(userID)
|
||||
if err != nil {
|
||||
logger.Error("[OPML:Export] %v", err)
|
||||
return "", errors.New("unable to fetch feeds")
|
||||
return "", err
|
||||
}
|
||||
|
||||
var subscriptions SubcriptionList
|
||||
@@ -74,7 +73,7 @@ func (h *Handler) Import(userID int64, data io.Reader) error {
|
||||
err := h.store.CreateCategory(category)
|
||||
if err != nil {
|
||||
logger.Error("[OPML:Import] %v", err)
|
||||
return fmt.Errorf(`unable to create this category: "%s"`, subscription.CategoryName)
|
||||
return fmt.Errorf(`unable to create this category: %q`, subscription.CategoryName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +280,12 @@ func isValidIframeSource(src string) bool {
|
||||
"https://www.dailymotion.com",
|
||||
"http://vk.com",
|
||||
"https://vk.com",
|
||||
"http://soundcloud.com",
|
||||
"https://soundcloud.com",
|
||||
"http://w.soundcloud.com",
|
||||
"https://w.soundcloud.com",
|
||||
"http://bandcamp.com",
|
||||
"https://bandcamp.com",
|
||||
}
|
||||
|
||||
for _, prefix := range whitelist {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/miniflux/miniflux/http"
|
||||
"github.com/miniflux/miniflux/http/client"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/reader/readability"
|
||||
"github.com/miniflux/miniflux/url"
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
|
||||
// Fetch downloads a web page a returns relevant contents.
|
||||
func Fetch(websiteURL, rules string) (string, error) {
|
||||
client := http.NewClient(websiteURL)
|
||||
response, err := client.Get()
|
||||
clt := client.New(websiteURL)
|
||||
response, err := clt.Get()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func scrapContent(page io.Reader, rules string) (string, error) {
|
||||
var content string
|
||||
|
||||
// For some inline elements, we get the parent.
|
||||
if s.Is("img") {
|
||||
if s.Is("img") || s.Is("iframe") {
|
||||
content, _ = s.Parent().Html()
|
||||
} else {
|
||||
content, _ = s.Html()
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/errors"
|
||||
"github.com/miniflux/miniflux/http"
|
||||
"github.com/miniflux/miniflux/http/client"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/reader/feed"
|
||||
"github.com/miniflux/miniflux/timer"
|
||||
@@ -30,8 +30,8 @@ var (
|
||||
func FindSubscriptions(websiteURL string) (Subscriptions, error) {
|
||||
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[FindSubscriptions] url=%s", websiteURL))
|
||||
|
||||
client := http.NewClient(websiteURL)
|
||||
response, err := client.Get()
|
||||
clt := client.New(websiteURL)
|
||||
response, err := clt.Get()
|
||||
if err != nil {
|
||||
if _, ok := err.(errors.LocalizedError); ok {
|
||||
return nil, err
|
||||
|
||||
@@ -27,14 +27,18 @@ func NewFeedScheduler(store *storage.Storage, workerPool *WorkerPool, frequency,
|
||||
}()
|
||||
}
|
||||
|
||||
// NewSessionScheduler starts a new scheduler that clean old sessions.
|
||||
func NewSessionScheduler(store *storage.Storage, frequency int) {
|
||||
// NewCleanupScheduler starts a new scheduler that clean old sessions and archive read items.
|
||||
func NewCleanupScheduler(store *storage.Storage, frequency int) {
|
||||
go func() {
|
||||
c := time.Tick(time.Duration(frequency) * time.Hour)
|
||||
for range c {
|
||||
nbSessions := store.CleanOldSessions()
|
||||
nbUserSessions := store.CleanOldUserSessions()
|
||||
logger.Info("[SessionScheduler] cleaned %d sessions and %d user sessions", nbSessions, nbUserSessions)
|
||||
logger.Info("[CleanupScheduler] Cleaned %d sessions and %d user sessions", nbSessions, nbUserSessions)
|
||||
|
||||
if err := store.ArchiveEntries(); err != nil {
|
||||
logger.Error("[CleanupScheduler] %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
alter table integrations add column pocket_enabled bool default 'f';
|
||||
alter table integrations add column pocket_access_token text default '';
|
||||
alter table integrations add column pocket_consumer_key text default '';
|
||||
@@ -0,0 +1 @@
|
||||
alter table user_sessions alter column ip set data type inet using ip::inet;
|
||||
+8
-1
@@ -1,5 +1,5 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2018-04-06 23:00:49.983090069 +0100 BST m=+0.002610702
|
||||
// 2018-05-21 12:33:12.475674107 -0700 PDT m=+0.003076147
|
||||
|
||||
package sql
|
||||
|
||||
@@ -131,6 +131,11 @@ alter table integrations add column nunux_keeper_url text default '';
|
||||
alter table integrations add column nunux_keeper_api_key text default '';`,
|
||||
"schema_version_15": `alter table enclosures alter column size set data type bigint;`,
|
||||
"schema_version_16": `alter table entries add column comments_url text default '';`,
|
||||
"schema_version_17": `alter table integrations add column pocket_enabled bool default 'f';
|
||||
alter table integrations add column pocket_access_token text default '';
|
||||
alter table integrations add column pocket_consumer_key text default '';
|
||||
`,
|
||||
"schema_version_18": `alter table user_sessions alter column ip set data type inet using ip::inet;`,
|
||||
"schema_version_2": `create extension if not exists hstore;
|
||||
alter table users add column extra hstore;
|
||||
create index users_extra_idx on users using gin(extra);
|
||||
@@ -178,6 +183,8 @@ var SqlMapChecksums = map[string]string{
|
||||
"schema_version_14": "4622e42c4a5a88b6fe1e61f3d367b295968f7260ab5b96481760775ba9f9e1fe",
|
||||
"schema_version_15": "13ff91462bdf4cda5a94a4c7a09f757761b0f2c32b4be713ba4786a4837750e4",
|
||||
"schema_version_16": "9d006faca62fd7ab787f64aef0e0a5933d142466ec4cab0e096bb920d2797e34",
|
||||
"schema_version_17": "b9f15d6217275fedcf6d948dd85ebe978b869bf37f42a86fd5b50a51919fa0e1",
|
||||
"schema_version_18": "c0ec24847612c7f2dc326cf735baffba79391a56aedd73292371a39f38724a71",
|
||||
"schema_version_2": "e8e9ff32478df04fcddad10a34cba2e8bb1e67e7977b5bd6cdc4c31ec94282b4",
|
||||
"schema_version_3": "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12",
|
||||
"schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
|
||||
|
||||
+2
-1
@@ -112,7 +112,8 @@ func (s *Storage) CategoriesWithFeedCount(userID int64) (model.Categories, error
|
||||
query := `SELECT
|
||||
c.id, c.user_id, c.title,
|
||||
(SELECT count(*) FROM feeds WHERE feeds.category_id=c.id) AS count
|
||||
FROM categories c WHERE user_id=$1`
|
||||
FROM categories c WHERE user_id=$1
|
||||
ORDER BY c.title ASC`
|
||||
|
||||
rows, err := s.db.Query(query, userID)
|
||||
if err != nil {
|
||||
|
||||
@@ -16,6 +16,20 @@ import (
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// CountUnreadEntries returns the number of unread entries.
|
||||
func (s *Storage) CountUnreadEntries(userID int64) int {
|
||||
builder := s.NewEntryQueryBuilder(userID)
|
||||
builder.WithStatus(model.EntryStatusUnread)
|
||||
|
||||
n, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
logger.Error("unable to count unread entries: %v", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// NewEntryQueryBuilder returns a new EntryQueryBuilder
|
||||
func (s *Storage) NewEntryQueryBuilder(userID int64) *EntryQueryBuilder {
|
||||
return NewEntryQueryBuilder(s, userID)
|
||||
@@ -162,6 +176,19 @@ func (s *Storage) cleanupEntries(feedID int64, entryHashes []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ArchiveEntries changes the status of read items to "removed" after 60 days.
|
||||
func (s *Storage) ArchiveEntries() error {
|
||||
query := `
|
||||
UPDATE entries SET status='removed'
|
||||
WHERE id=ANY(SELECT id FROM entries WHERE status='read' AND starred is false AND published_at < now () - '60 days'::interval LIMIT 500)
|
||||
`
|
||||
if _, err := s.db.Exec(query); err != nil {
|
||||
return fmt.Errorf("unable to archive read entries: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetEntriesStatus update the status of the given list of entries.
|
||||
func (s *Storage) SetEntriesStatus(userID int64, entryIDs []int64, status string) error {
|
||||
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:SetEntriesStatus] userID=%d, entryIDs=%v, status=%s", userID, entryIDs, status))
|
||||
|
||||
+30
-3
@@ -70,7 +70,10 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
|
||||
wallabag_password,
|
||||
nunux_keeper_enabled,
|
||||
nunux_keeper_url,
|
||||
nunux_keeper_api_key
|
||||
nunux_keeper_api_key,
|
||||
pocket_enabled,
|
||||
pocket_access_token,
|
||||
pocket_consumer_key
|
||||
FROM integrations
|
||||
WHERE user_id=$1
|
||||
`
|
||||
@@ -97,6 +100,9 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
|
||||
&integration.NunuxKeeperEnabled,
|
||||
&integration.NunuxKeeperURL,
|
||||
&integration.NunuxKeeperAPIKey,
|
||||
&integration.PocketEnabled,
|
||||
&integration.PocketAccessToken,
|
||||
&integration.PocketConsumerKey,
|
||||
)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
@@ -131,8 +137,11 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
|
||||
wallabag_password=$17,
|
||||
nunux_keeper_enabled=$18,
|
||||
nunux_keeper_url=$19,
|
||||
nunux_keeper_api_key=$20
|
||||
WHERE user_id=$21
|
||||
nunux_keeper_api_key=$20,
|
||||
pocket_enabled=$21,
|
||||
pocket_access_token=$22,
|
||||
pocket_consumer_key=$23
|
||||
WHERE user_id=$24
|
||||
`
|
||||
_, err := s.db.Exec(
|
||||
query,
|
||||
@@ -156,6 +165,9 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
|
||||
integration.NunuxKeeperEnabled,
|
||||
integration.NunuxKeeperURL,
|
||||
integration.NunuxKeeperAPIKey,
|
||||
integration.PocketEnabled,
|
||||
integration.PocketAccessToken,
|
||||
integration.PocketConsumerKey,
|
||||
integration.UserID,
|
||||
)
|
||||
|
||||
@@ -176,3 +188,18 @@ func (s *Storage) CreateIntegration(userID int64) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasSaveEntry returns true if the given user can save articles to third-parties.
|
||||
func (s *Storage) HasSaveEntry(userID int64) (result bool) {
|
||||
query := `
|
||||
SELECT true FROM integrations
|
||||
WHERE user_id=$1 AND
|
||||
(pinboard_enabled='t' OR instapaper_enabled='t' OR wallabag_enabled='t' OR nunux_keeper_enabled='t' OR pocket_enabled='t')
|
||||
`
|
||||
|
||||
if err := s.db.QueryRow(query, userID).Scan(&result); err != nil {
|
||||
result = false
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/miniflux/miniflux/sql"
|
||||
)
|
||||
|
||||
const schemaVersion = 16
|
||||
const schemaVersion = 18
|
||||
|
||||
// Migrate run database migrations.
|
||||
func (s *Storage) Migrate() {
|
||||
|
||||
@@ -339,6 +339,24 @@ func (s *Storage) CheckPassword(username, password string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasPassword returns true if the given user has a password defined.
|
||||
func (s *Storage) HasPassword(userID int64) (bool, error) {
|
||||
var result bool
|
||||
query := `SELECT true FROM users WHERE id=$1 AND password <> ''`
|
||||
|
||||
err := s.db.QueryRow(query, userID).Scan(&result)
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, fmt.Errorf("unable to execute query: %v", err)
|
||||
}
|
||||
|
||||
if result {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func hashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
|
||||
@@ -47,24 +47,20 @@ func (s *Storage) UserSessions(userID int64) (model.UserSessions, error) {
|
||||
}
|
||||
|
||||
// CreateUserSession creates a new sessions.
|
||||
func (s *Storage) CreateUserSession(username, userAgent, ip string) (sessionID string, err error) {
|
||||
var userID int64
|
||||
|
||||
func (s *Storage) CreateUserSession(username, userAgent, ip string) (sessionID string, userID int64, err error) {
|
||||
err = s.db.QueryRow("SELECT id FROM users WHERE username = LOWER($1)", username).Scan(&userID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to fetch UserID: %v", err)
|
||||
return "", 0, fmt.Errorf("unable to fetch user ID: %v", err)
|
||||
}
|
||||
|
||||
token := crypto.GenerateRandomString(64)
|
||||
query := "INSERT INTO user_sessions (token, user_id, user_agent, ip) VALUES ($1, $2, $3, $4)"
|
||||
_, err = s.db.Exec(query, token, userID, userAgent, ip)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to create user session: %v", err)
|
||||
return "", 0, fmt.Errorf("unable to create user session: %v", err)
|
||||
}
|
||||
|
||||
s.SetLastLogin(userID)
|
||||
|
||||
return token, nil
|
||||
return token, userID, nil
|
||||
}
|
||||
|
||||
// UserSessionByToken finds a session by the token.
|
||||
|
||||
+24
-21
@@ -1,5 +1,5 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2018-04-07 13:51:33.926223471 -0700 PDT m=+0.022666283
|
||||
// 2018-05-21 12:40:10.613112 +0200 CEST m=+0.010961753
|
||||
|
||||
package template
|
||||
|
||||
@@ -32,18 +32,25 @@ var templateCommonMap = map[string]string{
|
||||
<li>
|
||||
<time datetime="{{ isodate .entry.Date }}" title="{{ isodate .entry.Date }}">{{ elapsed .user.Timezone .entry.Date }}</time>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
title="{{ t "Save this article" }}"
|
||||
data-save-entry="true"
|
||||
data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
|
||||
data-label-loading="{{ t "Saving..." }}"
|
||||
data-label-done="{{ t "Done!" }}"
|
||||
>{{ t "Save" }}</a>
|
||||
</li>
|
||||
{{ if .hasSaveEntry }}
|
||||
<li>
|
||||
<a href="#"
|
||||
title="{{ t "Save this article" }}"
|
||||
data-save-entry="true"
|
||||
data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
|
||||
data-label-loading="{{ t "Saving..." }}"
|
||||
data-label-done="{{ t "Done!" }}"
|
||||
>{{ t "Save" }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
<li>
|
||||
<a href="{{ .entry.URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
|
||||
</li>
|
||||
{{ if .entry.CommentsURL }}
|
||||
<li>
|
||||
<a href="{{ .entry.CommentsURL }}" title="{{ t "View Comments" }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ t "Comments" }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
<li>
|
||||
<a href="#"
|
||||
data-toggle-bookmark="true"
|
||||
@@ -63,11 +70,6 @@ var templateCommonMap = map[string]string{
|
||||
data-value="{{ if eq .entry.Status "read" }}read{{ else }}unread{{ end }}"
|
||||
>{{ if eq .entry.Status "read" }}✘ {{ t "Unread" }}{{ else }}✔ {{ t "Read" }}{{ end }}</a>
|
||||
</li>
|
||||
{{ if .entry.CommentsURL }}
|
||||
<li>
|
||||
<a href="{{ .entry.CommentsURL }}" title="{{ t "View Comments" }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ t "Comments" }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}`,
|
||||
@@ -113,10 +115,11 @@ var templateCommonMap = map[string]string{
|
||||
</div>
|
||||
<ul>
|
||||
<li {{ if eq .menu "unread" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g u" }}">
|
||||
<a href="{{ route "unread" }}" data-page="unread">{{ t "Unread" }}</a>
|
||||
{{ if gt .countUnread 0 }}
|
||||
<span class="unread-counter-wrapper">(<span class="unread-counter">{{ .countUnread }}</span>)</span>
|
||||
{{ end }}
|
||||
<a href="{{ route "unread" }}" data-page="unread">{{ t "Unread" }}
|
||||
{{ if gt .countUnread 0 }}
|
||||
<span class="unread-counter-wrapper">(<span class="unread-counter">{{ .countUnread }}</span>)</span>
|
||||
{{ end }}
|
||||
</a>
|
||||
</li>
|
||||
<li {{ if eq .menu "starred" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g b" }}">
|
||||
<a href="{{ route "starred" }}" data-page="starred">{{ t "Starred" }}</a>
|
||||
@@ -220,7 +223,7 @@ var templateCommonMap = map[string]string{
|
||||
|
||||
var templateCommonMapChecksums = map[string]string{
|
||||
"entry_pagination": "f1465fa70f585ae8043b200ec9de5bf437ffbb0c19fb7aefc015c3555614ee27",
|
||||
"item_meta": "c14b5b36076e087346def2ebcef4876ed5e8165218f40dc017db44e754c22d03",
|
||||
"layout": "c7565e2cf904612e236bc1d7167c6c124ffe5d27348608eb5c2336606f266896",
|
||||
"item_meta": "6cff8ae243f19dac936e523867d2975f70aa749b2a461ae63f6ebbca94cf7419",
|
||||
"layout": "2226ad243581d200b3d9499787a5ba38d6b2fff38d2ce86a40eb395a6efc17de",
|
||||
"pagination": "6ff462c2b2a53bc5448b651da017f40a39f1d4f16cef4b2f09784f0797286924",
|
||||
}
|
||||
|
||||
+30
-10
@@ -7,9 +7,10 @@ package template
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/config"
|
||||
"github.com/miniflux/miniflux/errors"
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
|
||||
@@ -35,25 +36,44 @@ func (e *Engine) parseAll() {
|
||||
}
|
||||
}
|
||||
|
||||
// SetLanguage change the language for template processing.
|
||||
func (e *Engine) SetLanguage(language string) {
|
||||
e.funcMap.Language = e.translator.GetLanguage(language)
|
||||
}
|
||||
|
||||
// Execute process a template.
|
||||
func (e *Engine) Execute(w io.Writer, name string, data interface{}) {
|
||||
// Render process a template and write the ouput.
|
||||
func (e *Engine) Render(name, language string, data interface{}) []byte {
|
||||
tpl, ok := e.templates[name]
|
||||
if !ok {
|
||||
logger.Fatal("[Template] The template %s does not exists", name)
|
||||
}
|
||||
|
||||
lang := e.translator.GetLanguage(language)
|
||||
tpl.Funcs(template.FuncMap{
|
||||
"elapsed": func(timezone string, t time.Time) string {
|
||||
return elapsedTime(lang, timezone, t)
|
||||
},
|
||||
"t": func(key interface{}, args ...interface{}) string {
|
||||
switch key.(type) {
|
||||
case string:
|
||||
return lang.Get(key.(string), args...)
|
||||
case errors.LocalizedError:
|
||||
return key.(errors.LocalizedError).Localize(lang)
|
||||
case *errors.LocalizedError:
|
||||
return key.(*errors.LocalizedError).Localize(lang)
|
||||
case error:
|
||||
return key.(error).Error()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
},
|
||||
"plural": func(key string, n int, args ...interface{}) string {
|
||||
return lang.Plural(key, n, args...)
|
||||
},
|
||||
})
|
||||
|
||||
var b bytes.Buffer
|
||||
err := tpl.ExecuteTemplate(&b, "base", data)
|
||||
if err != nil {
|
||||
logger.Fatal("[Template] Unable to render template: %v", err)
|
||||
}
|
||||
|
||||
b.WriteTo(w)
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
// NewEngine returns a new template engine.
|
||||
@@ -61,7 +81,7 @@ func NewEngine(cfg *config.Config, router *mux.Router, translator *locale.Transl
|
||||
tpl := &Engine{
|
||||
templates: make(map[string]*template.Template),
|
||||
translator: translator,
|
||||
funcMap: newFuncMap(cfg, router, translator.GetLanguage("en_US")),
|
||||
funcMap: newFuncMap(cfg, router),
|
||||
}
|
||||
|
||||
tpl.parseAll()
|
||||
|
||||
+10
-22
@@ -12,17 +12,14 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/miniflux/miniflux/config"
|
||||
"github.com/miniflux/miniflux/errors"
|
||||
"github.com/miniflux/miniflux/filter"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
"github.com/miniflux/miniflux/url"
|
||||
)
|
||||
|
||||
type funcMap struct {
|
||||
cfg *config.Config
|
||||
router *mux.Router
|
||||
Language *locale.Language
|
||||
cfg *config.Config
|
||||
router *mux.Router
|
||||
}
|
||||
|
||||
func (f *funcMap) Map() template.FuncMap {
|
||||
@@ -77,30 +74,21 @@ func (f *funcMap) Map() template.FuncMap {
|
||||
"isodate": func(ts time.Time) string {
|
||||
return ts.Format("2006-01-02 15:04:05")
|
||||
},
|
||||
"dict": dict,
|
||||
|
||||
// These functions are overrided at runtime after the parsing.
|
||||
"elapsed": func(timezone string, t time.Time) string {
|
||||
return elapsedTime(f.Language, timezone, t)
|
||||
return ""
|
||||
},
|
||||
"t": func(key interface{}, args ...interface{}) string {
|
||||
switch key.(type) {
|
||||
case string:
|
||||
return f.Language.Get(key.(string), args...)
|
||||
case errors.LocalizedError:
|
||||
return key.(errors.LocalizedError).Localize(f.Language)
|
||||
case *errors.LocalizedError:
|
||||
return key.(*errors.LocalizedError).Localize(f.Language)
|
||||
case error:
|
||||
return key.(error).Error()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
},
|
||||
"plural": func(key string, n int, args ...interface{}) string {
|
||||
return f.Language.Plural(key, n, args...)
|
||||
return ""
|
||||
},
|
||||
"dict": dict,
|
||||
}
|
||||
}
|
||||
|
||||
func newFuncMap(cfg *config.Config, router *mux.Router, language *locale.Language) *funcMap {
|
||||
return &funcMap{cfg, router, language}
|
||||
func newFuncMap(cfg *config.Config, router *mux.Router) *funcMap {
|
||||
return &funcMap{cfg, router}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</span>
|
||||
<span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
|
||||
</div>
|
||||
{{ template "item_meta" dict "user" $.user "entry" . }}
|
||||
{{ template "item_meta" dict "user" $.user "entry" . "hasSaveEntry" $.hasSaveEntry }}
|
||||
</article>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -27,7 +27,7 @@
|
||||
</span>
|
||||
<span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
|
||||
</div>
|
||||
{{ template "item_meta" dict "user" $.user "entry" . }}
|
||||
{{ template "item_meta" dict "user" $.user "entry" . "hasSaveEntry" $.hasSaveEntry }}
|
||||
</article>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
@@ -7,18 +7,25 @@
|
||||
<li>
|
||||
<time datetime="{{ isodate .entry.Date }}" title="{{ isodate .entry.Date }}">{{ elapsed .user.Timezone .entry.Date }}</time>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
title="{{ t "Save this article" }}"
|
||||
data-save-entry="true"
|
||||
data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
|
||||
data-label-loading="{{ t "Saving..." }}"
|
||||
data-label-done="{{ t "Done!" }}"
|
||||
>{{ t "Save" }}</a>
|
||||
</li>
|
||||
{{ if .hasSaveEntry }}
|
||||
<li>
|
||||
<a href="#"
|
||||
title="{{ t "Save this article" }}"
|
||||
data-save-entry="true"
|
||||
data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
|
||||
data-label-loading="{{ t "Saving..." }}"
|
||||
data-label-done="{{ t "Done!" }}"
|
||||
>{{ t "Save" }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
<li>
|
||||
<a href="{{ .entry.URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
|
||||
</li>
|
||||
{{ if .entry.CommentsURL }}
|
||||
<li>
|
||||
<a href="{{ .entry.CommentsURL }}" title="{{ t "View Comments" }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ t "Comments" }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
<li>
|
||||
<a href="#"
|
||||
data-toggle-bookmark="true"
|
||||
@@ -38,11 +45,6 @@
|
||||
data-value="{{ if eq .entry.Status "read" }}read{{ else }}unread{{ end }}"
|
||||
>{{ if eq .entry.Status "read" }}✘ {{ t "Unread" }}{{ else }}✔ {{ t "Read" }}{{ end }}</a>
|
||||
</li>
|
||||
{{ if .entry.CommentsURL }}
|
||||
<li>
|
||||
<a href="{{ .entry.CommentsURL }}" title="{{ t "View Comments" }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ t "Comments" }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -40,10 +40,11 @@
|
||||
</div>
|
||||
<ul>
|
||||
<li {{ if eq .menu "unread" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g u" }}">
|
||||
<a href="{{ route "unread" }}" data-page="unread">{{ t "Unread" }}</a>
|
||||
{{ if gt .countUnread 0 }}
|
||||
<span class="unread-counter-wrapper">(<span class="unread-counter">{{ .countUnread }}</span>)</span>
|
||||
{{ end }}
|
||||
<a href="{{ route "unread" }}" data-page="unread">{{ t "Unread" }}
|
||||
{{ if gt .countUnread 0 }}
|
||||
<span class="unread-counter-wrapper">(<span class="unread-counter">{{ .countUnread }}</span>)</span>
|
||||
{{ end }}
|
||||
</a>
|
||||
</li>
|
||||
<li {{ if eq .menu "starred" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g b" }}">
|
||||
<a href="{{ route "starred" }}" data-page="starred">{{ t "Starred" }}</a>
|
||||
|
||||
@@ -18,15 +18,17 @@
|
||||
data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
|
||||
>{{ if .entry.Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
title="{{ t "Save this article" }}"
|
||||
data-save-entry="true"
|
||||
data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
|
||||
data-label-loading="{{ t "Saving..." }}"
|
||||
data-label-done="{{ t "Done!" }}"
|
||||
>{{ t "Save" }}</a>
|
||||
</li>
|
||||
{{ if .hasSaveEntry }}
|
||||
<li>
|
||||
<a href="#"
|
||||
title="{{ t "Save this article" }}"
|
||||
data-save-entry="true"
|
||||
data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
|
||||
data-label-loading="{{ t "Saving..." }}"
|
||||
data-label-done="{{ t "Done!" }}"
|
||||
>{{ t "Save" }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
<li>
|
||||
<a href="#"
|
||||
title="{{ t "Fetch original content" }}"
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</span>
|
||||
<span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
|
||||
</div>
|
||||
{{ template "item_meta" dict "user" $.user "entry" . }}
|
||||
{{ template "item_meta" dict "user" $.user "entry" . "hasSaveEntry" $.hasSaveEntry }}
|
||||
</article>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</span>
|
||||
<span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
|
||||
</div>
|
||||
{{ template "item_meta" dict "user" $.user "entry" . }}
|
||||
{{ template "item_meta" dict "user" $.user "entry" . "hasSaveEntry" $.hasSaveEntry }}
|
||||
</article>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -73,6 +73,25 @@
|
||||
<input type="password" name="instapaper_password" id="form-instapaper-password" value="{{ .form.InstapaperPassword }}">
|
||||
</div>
|
||||
|
||||
<h3>Pocket</h3>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="pocket_enabled" value="1" {{ if .form.PocketEnabled }}checked{{ end }}> {{ t "Save articles to Pocket" }}
|
||||
</label>
|
||||
|
||||
{{ if not .hasPocketConsumerKeyConfigured }}
|
||||
<label for="form-pocket-consumer-key">{{ t "Pocket Consumer Key" }}</label>
|
||||
<input type="text" name="pocket_consumer_key" id="form-pocket-consumer-key" value="{{ .form.PocketConsumerKey }}">
|
||||
{{ end }}
|
||||
|
||||
<label for="form-pocket-access-token">{{ t "Pocket Access Token" }}</label>
|
||||
<input type="password" name="pocket_access_token" id="form-pocket-access-token" value="{{ .form.PocketAccessToken }}">
|
||||
|
||||
{{ if not .form.PocketAccessToken }}
|
||||
<p><a href="{{ route "pocketAuthorize" }}">{{ t "Connect your Pocket account" }}</a></p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<h3>Wallabag</h3>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
@@ -94,7 +113,7 @@
|
||||
<label for="form-wallabag-password">{{ t "Wallabag Password" }}</label>
|
||||
<input type="password" name="wallabag_password" id="form-wallabag-password" value="{{ .form.WallabagPassword }}">
|
||||
</div>
|
||||
|
||||
|
||||
<h3>Nunux Keeper</h3>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
</span>
|
||||
<span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
|
||||
</div>
|
||||
{{ template "item_meta" dict "user" $.user "entry" . }}
|
||||
{{ template "item_meta" dict "user" $.user "entry" . "hasSaveEntry" $.hasSaveEntry }}
|
||||
</article>
|
||||
{{ end }}
|
||||
</div>
|
||||
+76
-55
@@ -1,5 +1,5 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2018-04-07 13:51:33.918407222 -0700 PDT m=+0.014850034
|
||||
// 2018-05-21 12:14:59.39668234 -0700 PDT m=+0.007850936
|
||||
|
||||
package template
|
||||
|
||||
@@ -91,6 +91,37 @@ var templateViewsMap = map[string]string{
|
||||
</form>
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
`,
|
||||
"bookmark_entries": `{{ define "title"}}{{ t "Favorites" }} ({{ .total }}){{ end }}
|
||||
|
||||
{{ define "content"}}
|
||||
<section class="page-header">
|
||||
<h1>{{ t "Favorites" }} ({{ .total }})</h1>
|
||||
</section>
|
||||
|
||||
{{ if not .entries }}
|
||||
<p class="alert alert-info">{{ t "There is no bookmark at the moment." }}</p>
|
||||
{{ else }}
|
||||
<div class="items">
|
||||
{{ range .entries }}
|
||||
<article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
|
||||
<div class="item-header">
|
||||
<span class="item-title">
|
||||
{{ if ne .Feed.Icon.IconID 0 }}
|
||||
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16">
|
||||
{{ end }}
|
||||
<a href="{{ route "starredEntry" "entryID" .ID }}">{{ .Title }}</a>
|
||||
</span>
|
||||
<span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
|
||||
</div>
|
||||
{{ template "item_meta" dict "user" $.user "entry" . "hasSaveEntry" $.hasSaveEntry }}
|
||||
</article>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ template "pagination" .pagination }}
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
`,
|
||||
"categories": `{{ define "title"}}{{ t "Categories" }} ({{ .total }}){{ end }}
|
||||
@@ -179,7 +210,7 @@ var templateViewsMap = map[string]string{
|
||||
</span>
|
||||
<span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
|
||||
</div>
|
||||
{{ template "item_meta" dict "user" $.user "entry" . }}
|
||||
{{ template "item_meta" dict "user" $.user "entry" . "hasSaveEntry" $.hasSaveEntry }}
|
||||
</article>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -483,15 +514,17 @@ var templateViewsMap = map[string]string{
|
||||
data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
|
||||
>{{ if .entry.Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
title="{{ t "Save this article" }}"
|
||||
data-save-entry="true"
|
||||
data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
|
||||
data-label-loading="{{ t "Saving..." }}"
|
||||
data-label-done="{{ t "Done!" }}"
|
||||
>{{ t "Save" }}</a>
|
||||
</li>
|
||||
{{ if .hasSaveEntry }}
|
||||
<li>
|
||||
<a href="#"
|
||||
title="{{ t "Save this article" }}"
|
||||
data-save-entry="true"
|
||||
data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
|
||||
data-label-loading="{{ t "Saving..." }}"
|
||||
data-label-done="{{ t "Done!" }}"
|
||||
>{{ t "Save" }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
<li>
|
||||
<a href="#"
|
||||
title="{{ t "Fetch original content" }}"
|
||||
@@ -618,7 +651,7 @@ var templateViewsMap = map[string]string{
|
||||
</span>
|
||||
<span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
|
||||
</div>
|
||||
{{ template "item_meta" dict "user" $.user "entry" . }}
|
||||
{{ template "item_meta" dict "user" $.user "entry" . "hasSaveEntry" $.hasSaveEntry }}
|
||||
</article>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -705,7 +738,7 @@ var templateViewsMap = map[string]string{
|
||||
|
||||
{{ end }}
|
||||
`,
|
||||
"history": `{{ define "title"}}{{ t "History" }} ({{ .total }}){{ end }}
|
||||
"history_entries": `{{ define "title"}}{{ t "History" }} ({{ .total }}){{ end }}
|
||||
|
||||
{{ define "content"}}
|
||||
<section class="page-header">
|
||||
@@ -734,7 +767,7 @@ var templateViewsMap = map[string]string{
|
||||
</span>
|
||||
<span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
|
||||
</div>
|
||||
{{ template "item_meta" dict "user" $.user "entry" . }}
|
||||
{{ template "item_meta" dict "user" $.user "entry" . "hasSaveEntry" $.hasSaveEntry }}
|
||||
</article>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -853,6 +886,25 @@ var templateViewsMap = map[string]string{
|
||||
<input type="password" name="instapaper_password" id="form-instapaper-password" value="{{ .form.InstapaperPassword }}">
|
||||
</div>
|
||||
|
||||
<h3>Pocket</h3>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="pocket_enabled" value="1" {{ if .form.PocketEnabled }}checked{{ end }}> {{ t "Save articles to Pocket" }}
|
||||
</label>
|
||||
|
||||
{{ if not .hasPocketConsumerKeyConfigured }}
|
||||
<label for="form-pocket-consumer-key">{{ t "Pocket Consumer Key" }}</label>
|
||||
<input type="text" name="pocket_consumer_key" id="form-pocket-consumer-key" value="{{ .form.PocketConsumerKey }}">
|
||||
{{ end }}
|
||||
|
||||
<label for="form-pocket-access-token">{{ t "Pocket Access Token" }}</label>
|
||||
<input type="password" name="pocket_access_token" id="form-pocket-access-token" value="{{ .form.PocketAccessToken }}">
|
||||
|
||||
{{ if not .form.PocketAccessToken }}
|
||||
<p><a href="{{ route "pocketAuthorize" }}">{{ t "Connect your Pocket account" }}</a></p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<h3>Wallabag</h3>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
@@ -874,7 +926,7 @@ var templateViewsMap = map[string]string{
|
||||
<label for="form-wallabag-password">{{ t "Wallabag Password" }}</label>
|
||||
<input type="password" name="wallabag_password" id="form-wallabag-password" value="{{ .form.WallabagPassword }}">
|
||||
</div>
|
||||
|
||||
|
||||
<h3>Nunux Keeper</h3>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
@@ -1090,38 +1142,7 @@ var templateViewsMap = map[string]string{
|
||||
|
||||
{{ end }}
|
||||
`,
|
||||
"starred": `{{ define "title"}}{{ t "Favorites" }} ({{ .total }}){{ end }}
|
||||
|
||||
{{ define "content"}}
|
||||
<section class="page-header">
|
||||
<h1>{{ t "Favorites" }} ({{ .total }})</h1>
|
||||
</section>
|
||||
|
||||
{{ if not .entries }}
|
||||
<p class="alert alert-info">{{ t "There is no bookmark at the moment." }}</p>
|
||||
{{ else }}
|
||||
<div class="items">
|
||||
{{ range .entries }}
|
||||
<article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
|
||||
<div class="item-header">
|
||||
<span class="item-title">
|
||||
{{ if ne .Feed.Icon.IconID 0 }}
|
||||
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16">
|
||||
{{ end }}
|
||||
<a href="{{ route "starredEntry" "entryID" .ID }}">{{ .Title }}</a>
|
||||
</span>
|
||||
<span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
|
||||
</div>
|
||||
{{ template "item_meta" dict "user" $.user "entry" . }}
|
||||
</article>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ template "pagination" .pagination }}
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
`,
|
||||
"unread": `{{ define "title"}}{{ t "Unread Items" }} {{ if gt .countUnread 0 }}({{ .countUnread }}){{ end }} {{ end }}
|
||||
"unread_entries": `{{ define "title"}}{{ t "Unread Items" }} {{ if gt .countUnread 0 }}({{ .countUnread }}){{ end }} {{ end }}
|
||||
|
||||
{{ define "content"}}
|
||||
<section class="page-header">
|
||||
@@ -1153,7 +1174,7 @@ var templateViewsMap = map[string]string{
|
||||
</span>
|
||||
<span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
|
||||
</div>
|
||||
{{ template "item_meta" dict "user" $.user "entry" . }}
|
||||
{{ template "item_meta" dict "user" $.user "entry" . "hasSaveEntry" $.hasSaveEntry }}
|
||||
</article>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -1230,24 +1251,24 @@ var templateViewsMap = map[string]string{
|
||||
var templateViewsMapChecksums = map[string]string{
|
||||
"about": "ad2fb778fc73c39b733b3f81b13e5c7d689b041fadd24ee2d4577f545aa788ad",
|
||||
"add_subscription": "053c920b0d7e109ea19dce6a448e304ce720db8633588ea04db16677f7209a7b",
|
||||
"bookmark_entries": "8e5fea7559218a34289c2f0e54955fc0ef3b9e629205927841cbcc2276aefb2a",
|
||||
"categories": "ca1280cd157bb527d4fc907da67b05a8347378f6dce965b9389d4bcdf3600a11",
|
||||
"category_entries": "686132d71c52a665329670756ac09959d915f7bc3227970149c623059988b035",
|
||||
"category_entries": "6ad52c8d0c28e21ea48be76228ea8432adde1dc190010753a48928477d52e065",
|
||||
"choose_subscription": "a325f9c976ca2b2dc148e25c8fef0cf6ccab0e04e86e604e7812bb18dc4cdde1",
|
||||
"create_category": "2b82af5d2dcd67898dc5daa57a6461e6ff8121a6089b2a2a1be909f35e4a2275",
|
||||
"create_user": "233764778c915754141a20429ec8db9bf80ef2d7704867a2d7232c1e9df233ae",
|
||||
"edit_category": "cee720faadcec58289b707ad30af623d2ee66c1ce23a732965463250d7ff41c5",
|
||||
"edit_feed": "d2c1c8486d7faf4ee58151ccf3e3c690e53bd6872050d291c5db8452a83c3d53",
|
||||
"edit_user": "321e0a60cf3bf7441bff970f4920e4c5b7c1883f80ab1d1674f8137954b25033",
|
||||
"entry": "656a33552c3662c5d38a6178c811316ff55b38460b1f57d4087d51d631efe896",
|
||||
"feed_entries": "420da786e827a77fecc8794207d158af3a30e489ca2b2019f48d5228919af4a7",
|
||||
"entry": "bd611521ebb46714fce434fe7fa5d4e53e50da4c3ed02450ad3557f614f16e14",
|
||||
"feed_entries": "4dffdb55cfad29df20612efe7ed2dbed03d919c4556898543ab6450f610d3c99",
|
||||
"feeds": "2a5abe37968ea34a0576dbef52341645cb1fc9562e351382fbf721491da6f4fa",
|
||||
"history": "967bc95236269ab3a77455910aca1939f43f93171fe1af77eb3b1b4eac579e55",
|
||||
"history_entries": "451f0b202f47c9db5344d3e73862f5b7afbd4323fbdba21b6087866c40f045d3",
|
||||
"import": "73b5112e20bfd232bf73334544186ea419505936bc237d481517a8622901878f",
|
||||
"integrations": "979193f39c2a3b43cec192aa119713cc9cbe2d5fdaedf8d2b3573c752823446c",
|
||||
"integrations": "4f93dab2630e6b3bf8bf51fc844e48fc763b8e7d6bb98c3c1692ea43f0e93930",
|
||||
"login": "7d83c3067c02f1f6aafdd8816c7f97a4eb5a5a4bdaaaa4cc1e2fbb9c17ea65e8",
|
||||
"sessions": "3fa79031dd883847eba92fbafe5f535fa3a4e1614bb610f20588b6f8fc8b3624",
|
||||
"settings": "ea2505b9d0a6d6bb594dba87a92079de19baa6d494f0651693a7685489fb7de9",
|
||||
"starred": "d006f5bcfca7abc7c68b6bb38fe25838acb81b65f960fbf06b97b259ba03b936",
|
||||
"unread": "ad71a7bdd46c1d650efecbeeb37b7606a046027c15284c0712912b77397f90d4",
|
||||
"unread_entries": "ca3ef1547d7d170b005a2f48fabd4c0a15550884db5e481659c13ffe6a47d19d",
|
||||
"users": "c6d91b0b29984b4cb3073bec6a2933cfb72981ec60f54b6c7aa05194f0e860bd",
|
||||
}
|
||||
|
||||
+21
-10
@@ -5,21 +5,32 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
"github.com/miniflux/miniflux/version"
|
||||
)
|
||||
|
||||
// AboutPage shows the about page.
|
||||
func (c *Controller) AboutPage(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
// About shows the about page.
|
||||
func (c *Controller) About(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("about", args.Merge(tplParams{
|
||||
"version": version.Version,
|
||||
"build_date": version.BuildDate,
|
||||
"menu": "settings",
|
||||
}))
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("version", version.Version)
|
||||
view.Set("build_date", version.BuildDate)
|
||||
view.Set("menu", "settings")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("about"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// ShowStarredPage renders the page with all starred entries.
|
||||
func (c *Controller) ShowStarredPage(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
offset := request.QueryIntParam(r, "offset", 0)
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithStarred()
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(nbItemsPerPage)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
|
||||
view.Set("total", count)
|
||||
view.Set("entries", entries)
|
||||
view.Set("pagination", c.getPagination(route.Path(c.router, "starred"), count, offset))
|
||||
view.Set("menu", "starred")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
|
||||
|
||||
html.OK(w, view.Render("bookmark_entries"))
|
||||
}
|
||||
-257
@@ -1,257 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
)
|
||||
|
||||
// ShowCategories shows the page with all categories.
|
||||
func (c *Controller) ShowCategories(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.LoggedUser()
|
||||
categories, err := c.store.CategoriesWithFeedCount(user.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("categories", args.Merge(tplParams{
|
||||
"categories": categories,
|
||||
"total": len(categories),
|
||||
"menu": "categories",
|
||||
}))
|
||||
}
|
||||
|
||||
// ShowCategoryEntries shows all entries for the given category.
|
||||
func (c *Controller) ShowCategoryEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
offset := request.QueryIntegerParam("offset", 0)
|
||||
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := c.getCategoryFromURL(ctx, request, response)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithCategoryID(category.ID)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(nbItemsPerPage)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("category_entries", args.Merge(tplParams{
|
||||
"category": category,
|
||||
"entries": entries,
|
||||
"total": count,
|
||||
"pagination": c.getPagination(ctx.Route("categoryEntries", "categoryID", category.ID), count, offset),
|
||||
"menu": "categories",
|
||||
}))
|
||||
}
|
||||
|
||||
// CreateCategory shows the form to create a new category.
|
||||
func (c *Controller) CreateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("create_category", args.Merge(tplParams{
|
||||
"menu": "categories",
|
||||
}))
|
||||
}
|
||||
|
||||
// SaveCategory validate and save the new category into the database.
|
||||
func (c *Controller) SaveCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
categoryForm := form.NewCategoryForm(request.Request())
|
||||
if err := categoryForm.Validate(); err != nil {
|
||||
response.HTML().Render("create_category", args.Merge(tplParams{
|
||||
"errorMessage": err.Error(),
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
duplicateCategory, err := c.store.CategoryByTitle(user.ID, categoryForm.Title)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if duplicateCategory != nil {
|
||||
response.HTML().Render("create_category", args.Merge(tplParams{
|
||||
"errorMessage": "This category already exists.",
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
category := model.Category{Title: categoryForm.Title, UserID: user.ID}
|
||||
err = c.store.CreateCategory(&category)
|
||||
if err != nil {
|
||||
logger.Info("[Controller:CreateCategory] %v", err)
|
||||
response.HTML().Render("create_category", args.Merge(tplParams{
|
||||
"errorMessage": "Unable to create this category.",
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(ctx.Route("categories"))
|
||||
}
|
||||
|
||||
// EditCategory shows the form to modify a category.
|
||||
func (c *Controller) EditCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
category, err := c.getCategoryFromURL(ctx, request, response)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:EditCategory] %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
args, err := c.getCategoryFormTemplateArgs(ctx, user, category, nil)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("edit_category", args)
|
||||
}
|
||||
|
||||
// UpdateCategory validate and update a category.
|
||||
func (c *Controller) UpdateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
category, err := c.getCategoryFromURL(ctx, request, response)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:UpdateCategory] %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
categoryForm := form.NewCategoryForm(request.Request())
|
||||
args, err := c.getCategoryFormTemplateArgs(ctx, user, category, categoryForm)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := categoryForm.Validate(); err != nil {
|
||||
response.HTML().Render("edit_category", args.Merge(tplParams{
|
||||
"errorMessage": err.Error(),
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
if c.store.AnotherCategoryExists(user.ID, category.ID, categoryForm.Title) {
|
||||
response.HTML().Render("edit_category", args.Merge(tplParams{
|
||||
"errorMessage": "This category already exists.",
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.UpdateCategory(categoryForm.Merge(category))
|
||||
if err != nil {
|
||||
logger.Error("[Controller:UpdateCategory] %v", err)
|
||||
response.HTML().Render("edit_category", args.Merge(tplParams{
|
||||
"errorMessage": "Unable to update this category.",
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(ctx.Route("categories"))
|
||||
}
|
||||
|
||||
// RemoveCategory delete a category from the database.
|
||||
func (c *Controller) RemoveCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
category, err := c.getCategoryFromURL(ctx, request, response)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.RemoveCategory(user.ID, category.ID); err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(ctx.Route("categories"))
|
||||
}
|
||||
|
||||
func (c *Controller) getCategoryFromURL(ctx *handler.Context, request *handler.Request, response *handler.Response) (*model.Category, error) {
|
||||
categoryID, err := request.IntegerParam("categoryID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := ctx.LoggedUser()
|
||||
category, err := c.store.Category(user.ID, categoryID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if category == nil {
|
||||
response.HTML().NotFound()
|
||||
return nil, errors.New("Category not found")
|
||||
}
|
||||
|
||||
return category, nil
|
||||
}
|
||||
|
||||
func (c *Controller) getCategoryFormTemplateArgs(ctx *handler.Context, user *model.User, category *model.Category, categoryForm *form.CategoryForm) (tplParams, error) {
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if categoryForm == nil {
|
||||
args["form"] = form.CategoryForm{
|
||||
Title: category.Title,
|
||||
}
|
||||
} else {
|
||||
args["form"] = categoryForm
|
||||
}
|
||||
|
||||
args["category"] = category
|
||||
args["menu"] = "categories"
|
||||
return args, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// CreateCategory shows the form to create a new category.
|
||||
func (c *Controller) CreateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("menu", "categories")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("create_category"))
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// EditCategory shows the form to modify a category.
|
||||
func (c *Controller) EditCategory(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
categoryID, err := request.IntParam(r, "categoryID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := c.store.Category(ctx.UserID(), categoryID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if category == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
categoryForm := form.CategoryForm{
|
||||
Title: category.Title,
|
||||
}
|
||||
|
||||
view.Set("form", categoryForm)
|
||||
view.Set("category", category)
|
||||
view.Set("menu", "categories")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("edit_category"))
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// CategoryEntries shows all entries for the given category.
|
||||
func (c *Controller) CategoryEntries(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
categoryID, err := request.IntParam(r, "categoryID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := c.store.Category(ctx.UserID(), categoryID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if category == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
offset := request.QueryIntParam(r, "offset", 0)
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithCategoryID(category.ID)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(nbItemsPerPage)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("category", category)
|
||||
view.Set("total", count)
|
||||
view.Set("entries", entries)
|
||||
view.Set("pagination", c.getPagination(route.Path(c.router, "categoryEntries", "categoryID", category.ID), count, offset))
|
||||
view.Set("menu", "categories")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
|
||||
|
||||
html.OK(w, view.Render("category_entries"))
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// CategoryList shows the page with all categories.
|
||||
func (c *Controller) CategoryList(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
categories, err := c.store.CategoriesWithFeedCount(user.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("categories", categories)
|
||||
view.Set("total", len(categories))
|
||||
view.Set("menu", "categories")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("categories"))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
)
|
||||
|
||||
// RemoveCategory deletes a category from the database.
|
||||
func (c *Controller) RemoveCategory(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
categoryID, err := request.IntParam(r, "categoryID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := c.store.Category(ctx.UserID(), categoryID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if category == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.RemoveCategory(user.ID, category.ID); err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(w, r, route.Path(c.router, "categories"))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user