Use vanilla HTTP handlers (refactoring)
This commit is contained in:
parent
1eba1730d1
commit
f49b42f70f
121 changed files with 4339 additions and 3369 deletions
|
@ -6,98 +6,105 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"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.
|
// CreateCategory is the API handler to create a new category.
|
||||||
func (c *Controller) CreateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) CreateCategory(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := ctx.UserID()
|
category, err := decodeCategoryPayload(r.Body)
|
||||||
category, err := decodeCategoryPayload(request.Body())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := context.New(r)
|
||||||
|
userID := ctx.UserID()
|
||||||
category.UserID = userID
|
category.UserID = userID
|
||||||
if err := category.ValidateCategoryCreation(); err != nil {
|
if err := category.ValidateCategoryCreation(); err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if c, err := c.store.CategoryByTitle(userID, category.Title); err != nil || c != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.store.CreateCategory(category)
|
err = c.store.CreateCategory(category)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(errors.New("Unable to create this category"))
|
json.ServerError(w, errors.New("Unable to create this category"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().Created(category)
|
json.Created(w, category)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCategory is the API handler to update a category.
|
// UpdateCategory is the API handler to update a category.
|
||||||
func (c *Controller) UpdateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) UpdateCategory(w http.ResponseWriter, r *http.Request) {
|
||||||
categoryID, err := request.IntegerParam("categoryID")
|
categoryID, err := request.IntParam(r, "categoryID")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
category, err := decodeCategoryPayload(request.Body())
|
category, err := decodeCategoryPayload(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := context.New(r)
|
||||||
category.UserID = ctx.UserID()
|
category.UserID = ctx.UserID()
|
||||||
category.ID = categoryID
|
category.ID = categoryID
|
||||||
if err := category.ValidateCategoryModification(); err != nil {
|
if err := category.ValidateCategoryModification(); err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.store.UpdateCategory(category)
|
err = c.store.UpdateCategory(category)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(errors.New("Unable to update this category"))
|
json.ServerError(w, errors.New("Unable to update this category"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().Created(category)
|
json.Created(w, category)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCategories is the API handler to get a list of categories for a given user.
|
// 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())
|
categories, err := c.store.Categories(ctx.UserID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(errors.New("Unable to fetch categories"))
|
json.ServerError(w, errors.New("Unable to fetch categories"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().Standard(categories)
|
json.OK(w, categories)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveCategory is the API handler to remove a category.
|
// 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()
|
userID := ctx.UserID()
|
||||||
categoryID, err := request.IntegerParam("categoryID")
|
categoryID, err := request.IntParam(r, "categoryID")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.store.CategoryExists(userID, categoryID) {
|
if !c.store.CategoryExists(userID, categoryID) {
|
||||||
response.JSON().NotFound(errors.New("Category not found"))
|
json.NotFound(w, errors.New("Category not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.store.RemoveCategory(userID, categoryID); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().NoContent()
|
json.NoContent(w)
|
||||||
}
|
}
|
||||||
|
|
132
api/entry.go
132
api/entry.go
|
@ -6,107 +6,110 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"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"
|
"github.com/miniflux/miniflux/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetFeedEntry is the API handler to get a single feed entry.
|
// GetFeedEntry is the API handler to get a single feed entry.
|
||||||
func (c *Controller) GetFeedEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) GetFeedEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := ctx.UserID()
|
feedID, err := request.IntParam(r, "feedID")
|
||||||
feedID, err := request.IntegerParam("feedID")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
entryID, err := request.IntegerParam("entryID")
|
entryID, err := request.IntParam(r, "entryID")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := context.New(r)
|
||||||
|
userID := ctx.UserID()
|
||||||
|
|
||||||
builder := c.store.NewEntryQueryBuilder(userID)
|
builder := c.store.NewEntryQueryBuilder(userID)
|
||||||
builder.WithFeedID(feedID)
|
builder.WithFeedID(feedID)
|
||||||
builder.WithEntryID(entryID)
|
builder.WithEntryID(entryID)
|
||||||
|
|
||||||
entry, err := builder.GetEntry()
|
entry, err := builder.GetEntry()
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
response.JSON().NotFound(errors.New("Entry not found"))
|
json.NotFound(w, errors.New("Entry not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().Standard(entry)
|
json.OK(w, entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEntry is the API handler to get a single entry.
|
// GetEntry is the API handler to get a single entry.
|
||||||
func (c *Controller) GetEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) GetEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := ctx.UserID()
|
entryID, err := request.IntParam(r, "entryID")
|
||||||
entryID, err := request.IntegerParam("entryID")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
builder := c.store.NewEntryQueryBuilder(userID)
|
builder := c.store.NewEntryQueryBuilder(context.New(r).UserID())
|
||||||
builder.WithEntryID(entryID)
|
builder.WithEntryID(entryID)
|
||||||
|
|
||||||
entry, err := builder.GetEntry()
|
entry, err := builder.GetEntry()
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
response.JSON().NotFound(errors.New("Entry not found"))
|
json.NotFound(w, errors.New("Entry not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().Standard(entry)
|
json.OK(w, entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFeedEntries is the API handler to get all feed entries.
|
// GetFeedEntries is the API handler to get all feed entries.
|
||||||
func (c *Controller) GetFeedEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) GetFeedEntries(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := ctx.UserID()
|
feedID, err := request.IntParam(r, "feedID")
|
||||||
feedID, err := request.IntegerParam("feedID")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
status := request.QueryStringParam("status", "")
|
status := request.QueryParam(r, "status", "")
|
||||||
if status != "" {
|
if status != "" {
|
||||||
if err := model.ValidateEntryStatus(status); err != nil {
|
if err := model.ValidateEntryStatus(status); err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
order := request.QueryStringParam("order", model.DefaultSortingOrder)
|
order := request.QueryParam(r, "order", model.DefaultSortingOrder)
|
||||||
if err := model.ValidateEntryOrder(order); err != nil {
|
if err := model.ValidateEntryOrder(order); err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
direction := request.QueryStringParam("direction", model.DefaultSortingDirection)
|
direction := request.QueryParam(r, "direction", model.DefaultSortingDirection)
|
||||||
if err := model.ValidateDirection(direction); err != nil {
|
if err := model.ValidateDirection(direction); err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
limit := request.QueryIntegerParam("limit", 100)
|
limit := request.QueryIntParam(r, "limit", 100)
|
||||||
offset := request.QueryIntegerParam("offset", 0)
|
offset := request.QueryIntParam(r, "offset", 0)
|
||||||
if err := model.ValidateRange(offset, limit); err != nil {
|
if err := model.ValidateRange(offset, limit); err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
builder := c.store.NewEntryQueryBuilder(userID)
|
builder := c.store.NewEntryQueryBuilder(context.New(r).UserID())
|
||||||
builder.WithFeedID(feedID)
|
builder.WithFeedID(feedID)
|
||||||
builder.WithStatus(status)
|
builder.WithStatus(status)
|
||||||
builder.WithOrder(order)
|
builder.WithOrder(order)
|
||||||
|
@ -116,51 +119,49 @@ func (c *Controller) GetFeedEntries(ctx *handler.Context, request *handler.Reque
|
||||||
|
|
||||||
entries, err := builder.GetEntries()
|
entries, err := builder.GetEntries()
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
count, err := builder.CountEntries()
|
count, err := builder.CountEntries()
|
||||||
if err != nil {
|
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
|
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.
|
// GetEntries is the API handler to fetch entries.
|
||||||
func (c *Controller) GetEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) GetEntries(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := ctx.UserID()
|
status := request.QueryParam(r, "status", "")
|
||||||
|
|
||||||
status := request.QueryStringParam("status", "")
|
|
||||||
if status != "" {
|
if status != "" {
|
||||||
if err := model.ValidateEntryStatus(status); err != nil {
|
if err := model.ValidateEntryStatus(status); err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
order := request.QueryStringParam("order", model.DefaultSortingOrder)
|
order := request.QueryParam(r, "order", model.DefaultSortingOrder)
|
||||||
if err := model.ValidateEntryOrder(order); err != nil {
|
if err := model.ValidateEntryOrder(order); err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
direction := request.QueryStringParam("direction", model.DefaultSortingDirection)
|
direction := request.QueryParam(r, "direction", model.DefaultSortingDirection)
|
||||||
if err := model.ValidateDirection(direction); err != nil {
|
if err := model.ValidateDirection(direction); err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
limit := request.QueryIntegerParam("limit", 100)
|
limit := request.QueryIntParam(r, "limit", 100)
|
||||||
offset := request.QueryIntegerParam("offset", 0)
|
offset := request.QueryIntParam(r, "offset", 0)
|
||||||
if err := model.ValidateRange(offset, limit); err != nil {
|
if err := model.ValidateRange(offset, limit); err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
builder := c.store.NewEntryQueryBuilder(userID)
|
builder := c.store.NewEntryQueryBuilder(context.New(r).UserID())
|
||||||
builder.WithStatus(status)
|
builder.WithStatus(status)
|
||||||
builder.WithOrder(order)
|
builder.WithOrder(order)
|
||||||
builder.WithDirection(direction)
|
builder.WithDirection(direction)
|
||||||
|
@ -169,55 +170,52 @@ func (c *Controller) GetEntries(ctx *handler.Context, request *handler.Request,
|
||||||
|
|
||||||
entries, err := builder.GetEntries()
|
entries, err := builder.GetEntries()
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
count, err := builder.CountEntries()
|
count, err := builder.CountEntries()
|
||||||
if err != nil {
|
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
|
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.
|
// SetEntryStatus is the API handler to change the status of entries.
|
||||||
func (c *Controller) SetEntryStatus(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) SetEntryStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := ctx.UserID()
|
entryIDs, status, err := decodeEntryStatusPayload(r.Body)
|
||||||
|
|
||||||
entryIDs, status, err := decodeEntryStatusPayload(request.Body())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(errors.New("Invalid JSON payload"))
|
json.BadRequest(w, errors.New("Invalid JSON payload"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := model.ValidateEntryStatus(status); err != nil {
|
if err := model.ValidateEntryStatus(status); err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.store.SetEntriesStatus(userID, entryIDs, status); err != nil {
|
if err := c.store.SetEntriesStatus(context.New(r).UserID(), entryIDs, status); err != nil {
|
||||||
response.JSON().ServerError(errors.New("Unable to change entries status"))
|
json.ServerError(w, errors.New("Unable to change entries status"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().NoContent()
|
json.NoContent(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToggleBookmark is the API handler to toggle bookmark status.
|
// ToggleBookmark is the API handler to toggle bookmark status.
|
||||||
func (c *Controller) ToggleBookmark(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) ToggleBookmark(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := ctx.UserID()
|
entryID, err := request.IntParam(r, "entryID")
|
||||||
entryID, err := request.IntegerParam("entryID")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.store.ToggleBookmark(userID, entryID); err != nil {
|
if err := c.store.ToggleBookmark(context.New(r).UserID(), entryID); err != nil {
|
||||||
response.JSON().ServerError(errors.New("Unable to toggle bookmark value"))
|
json.ServerError(w, errors.New("Unable to toggle bookmark value"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().NoContent()
|
json.NoContent(w)
|
||||||
}
|
}
|
||||||
|
|
119
api/feed.go
119
api/feed.go
|
@ -6,44 +6,49 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/miniflux/miniflux/http/context"
|
||||||
|
"github.com/miniflux/miniflux/http/request"
|
||||||
|
"github.com/miniflux/miniflux/http/response/json"
|
||||||
|
"github.com/miniflux/miniflux/http/response/xml"
|
||||||
"github.com/miniflux/miniflux/reader/opml"
|
"github.com/miniflux/miniflux/reader/opml"
|
||||||
|
|
||||||
"github.com/miniflux/miniflux/http/handler"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateFeed is the API handler to create a new feed.
|
// CreateFeed is the API handler to create a new feed.
|
||||||
func (c *Controller) CreateFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) CreateFeed(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := ctx.UserID()
|
feedURL, categoryID, err := decodeFeedCreationPayload(r.Body)
|
||||||
feedURL, categoryID, err := decodeFeedCreationPayload(request.Body())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if feedURL == "" {
|
if feedURL == "" {
|
||||||
response.JSON().BadRequest(errors.New("The feed_url is required"))
|
json.BadRequest(w, errors.New("The feed_url is required"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if categoryID <= 0 {
|
if categoryID <= 0 {
|
||||||
response.JSON().BadRequest(errors.New("The category_id is required"))
|
json.BadRequest(w, errors.New("The category_id is required"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := context.New(r)
|
||||||
|
userID := ctx.UserID()
|
||||||
|
|
||||||
if c.store.FeedURLExists(userID, feedURL) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.store.CategoryExists(userID, categoryID) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
feed, err := c.feedHandler.CreateFeed(userID, categoryID, feedURL, false)
|
feed, err := c.feedHandler.CreateFeed(userID, categoryID, feedURL, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(errors.New("Unable to create this feed"))
|
json.ServerError(w, errors.New("Unable to create this feed"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,142 +56,146 @@ func (c *Controller) CreateFeed(ctx *handler.Context, request *handler.Request,
|
||||||
FeedID int64 `json:"feed_id"`
|
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.
|
// RefreshFeed is the API handler to refresh a feed.
|
||||||
func (c *Controller) RefreshFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) RefreshFeed(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := ctx.UserID()
|
feedID, err := request.IntParam(r, "feedID")
|
||||||
feedID, err := request.IntegerParam("feedID")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := context.New(r)
|
||||||
|
userID := ctx.UserID()
|
||||||
|
|
||||||
if !c.store.FeedExists(userID, feedID) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.feedHandler.RefreshFeed(userID, feedID)
|
err = c.feedHandler.RefreshFeed(userID, feedID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(errors.New("Unable to refresh this feed"))
|
json.ServerError(w, errors.New("Unable to refresh this feed"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().NoContent()
|
json.NoContent(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateFeed is the API handler that is used to update a feed.
|
// 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) {
|
func (c *Controller) UpdateFeed(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := ctx.UserID()
|
feedID, err := request.IntParam(r, "feedID")
|
||||||
feedID, err := request.IntegerParam("feedID")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newFeed, err := decodeFeedModificationPayload(request.Body())
|
newFeed, err := decodeFeedModificationPayload(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := context.New(r)
|
||||||
|
userID := ctx.UserID()
|
||||||
|
|
||||||
if newFeed.Category != nil && newFeed.Category.ID != 0 && !c.store.CategoryExists(userID, newFeed.Category.ID) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
originalFeed, err := c.store.FeedByID(userID, feedID)
|
originalFeed, err := c.store.FeedByID(userID, feedID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().NotFound(errors.New("Unable to find this feed"))
|
json.NotFound(w, errors.New("Unable to find this feed"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if originalFeed == nil {
|
if originalFeed == nil {
|
||||||
response.JSON().NotFound(errors.New("Feed not found"))
|
json.NotFound(w, errors.New("Feed not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
originalFeed.Merge(newFeed)
|
originalFeed.Merge(newFeed)
|
||||||
if err := c.store.UpdateFeed(originalFeed); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
originalFeed, err = c.store.FeedByID(userID, feedID)
|
originalFeed, err = c.store.FeedByID(userID, feedID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(errors.New("Unable to fetch this feed"))
|
json.ServerError(w, errors.New("Unable to fetch this feed"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().Created(originalFeed)
|
json.Created(w, originalFeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFeeds is the API handler that get all feeds that belongs to the given user.
|
// 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) {
|
func (c *Controller) GetFeeds(w http.ResponseWriter, r *http.Request) {
|
||||||
feeds, err := c.store.Feeds(ctx.UserID())
|
feeds, err := c.store.Feeds(context.New(r).UserID())
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().Standard(feeds)
|
json.OK(w, feeds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export is the API handler that incoves an OPML export.
|
// Export is the API handler that incoves an OPML export.
|
||||||
func (c *Controller) Export(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) Export(w http.ResponseWriter, r *http.Request) {
|
||||||
opmlHandler := opml.NewHandler(c.store)
|
opmlHandler := opml.NewHandler(c.store)
|
||||||
|
opml, err := opmlHandler.Export(context.New(r).UserID())
|
||||||
opml, err := opmlHandler.Export(ctx.LoggedUser().ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(errors.New("unable to export feeds to OPML"))
|
json.ServerError(w, errors.New("unable to export feeds to OPML"))
|
||||||
}
|
}
|
||||||
|
|
||||||
response.XML().Serve(opml)
|
xml.OK(w, opml)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFeed is the API handler to get a feed.
|
// GetFeed is the API handler to get a feed.
|
||||||
func (c *Controller) GetFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) GetFeed(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := ctx.UserID()
|
feedID, err := request.IntParam(r, "feedID")
|
||||||
feedID, err := request.IntegerParam("feedID")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
feed, err := c.store.FeedByID(userID, feedID)
|
feed, err := c.store.FeedByID(context.New(r).UserID(), feedID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(errors.New("Unable to fetch this feed"))
|
json.ServerError(w, errors.New("Unable to fetch this feed"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if feed == nil {
|
if feed == nil {
|
||||||
response.JSON().NotFound(errors.New("Feed not found"))
|
json.NotFound(w, errors.New("Feed not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().Standard(feed)
|
json.OK(w, feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveFeed is the API handler to remove a feed.
|
// RemoveFeed is the API handler to remove a feed.
|
||||||
func (c *Controller) RemoveFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) RemoveFeed(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := ctx.UserID()
|
feedID, err := request.IntParam(r, "feedID")
|
||||||
feedID, err := request.IntegerParam("feedID")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := context.New(r)
|
||||||
|
userID := ctx.UserID()
|
||||||
|
|
||||||
if !c.store.FeedExists(userID, feedID) {
|
if !c.store.FeedExists(userID, feedID) {
|
||||||
response.JSON().NotFound(errors.New("Feed not found"))
|
json.NotFound(w, errors.New("Feed not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.store.RemoveFeed(userID, feedID); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().NoContent()
|
json.NoContent(w)
|
||||||
}
|
}
|
||||||
|
|
22
api/icon.go
22
api/icon.go
|
@ -6,36 +6,38 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"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.
|
// FeedIcon returns a feed icon.
|
||||||
func (c *Controller) FeedIcon(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) FeedIcon(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := ctx.UserID()
|
feedID, err := request.IntParam(r, "feedID")
|
||||||
feedID, err := request.IntegerParam("feedID")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.store.HasIcon(feedID) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
icon, err := c.store.IconByFeedID(userID, feedID)
|
icon, err := c.store.IconByFeedID(context.New(r).UserID(), feedID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(errors.New("Unable to fetch feed icon"))
|
json.ServerError(w, errors.New("Unable to fetch feed icon"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if icon == nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().Standard(&feedIcon{
|
json.OK(w, &feedIcon{
|
||||||
ID: icon.ID,
|
ID: icon.ID,
|
||||||
MimeType: icon.MimeType,
|
MimeType: icon.MimeType,
|
||||||
Data: icon.DataURL(),
|
Data: icon.DataURL(),
|
||||||
|
|
|
@ -23,10 +23,11 @@ type entriesResponse struct {
|
||||||
Entries model.Entries `json:"entries"`
|
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
|
var user model.User
|
||||||
|
|
||||||
decoder := json.NewDecoder(data)
|
decoder := json.NewDecoder(r)
|
||||||
|
defer r.Close()
|
||||||
if err := decoder.Decode(&user); err != nil {
|
if err := decoder.Decode(&user); err != nil {
|
||||||
return nil, fmt.Errorf("Unable to decode user JSON object: %v", err)
|
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
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeURLPayload(data io.Reader) (string, error) {
|
func decodeURLPayload(r io.ReadCloser) (string, error) {
|
||||||
type payload struct {
|
type payload struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var p payload
|
var p payload
|
||||||
decoder := json.NewDecoder(data)
|
decoder := json.NewDecoder(r)
|
||||||
|
defer r.Close()
|
||||||
if err := decoder.Decode(&p); err != nil {
|
if err := decoder.Decode(&p); err != nil {
|
||||||
return "", fmt.Errorf("invalid JSON payload: %v", err)
|
return "", fmt.Errorf("invalid JSON payload: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -48,14 +50,15 @@ func decodeURLPayload(data io.Reader) (string, error) {
|
||||||
return p.URL, nil
|
return p.URL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeEntryStatusPayload(data io.Reader) ([]int64, string, error) {
|
func decodeEntryStatusPayload(r io.ReadCloser) ([]int64, string, error) {
|
||||||
type payload struct {
|
type payload struct {
|
||||||
EntryIDs []int64 `json:"entry_ids"`
|
EntryIDs []int64 `json:"entry_ids"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var p payload
|
var p payload
|
||||||
decoder := json.NewDecoder(data)
|
decoder := json.NewDecoder(r)
|
||||||
|
defer r.Close()
|
||||||
if err := decoder.Decode(&p); err != nil {
|
if err := decoder.Decode(&p); err != nil {
|
||||||
return nil, "", fmt.Errorf("invalid JSON payload: %v", err)
|
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
|
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 {
|
type payload struct {
|
||||||
FeedURL string `json:"feed_url"`
|
FeedURL string `json:"feed_url"`
|
||||||
CategoryID int64 `json:"category_id"`
|
CategoryID int64 `json:"category_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var p payload
|
var p payload
|
||||||
decoder := json.NewDecoder(data)
|
decoder := json.NewDecoder(r)
|
||||||
|
defer r.Close()
|
||||||
if err := decoder.Decode(&p); err != nil {
|
if err := decoder.Decode(&p); err != nil {
|
||||||
return "", 0, fmt.Errorf("invalid JSON payload: %v", err)
|
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
|
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
|
var feed model.Feed
|
||||||
|
|
||||||
decoder := json.NewDecoder(data)
|
decoder := json.NewDecoder(r)
|
||||||
|
defer r.Close()
|
||||||
if err := decoder.Decode(&feed); err != nil {
|
if err := decoder.Decode(&feed); err != nil {
|
||||||
return nil, fmt.Errorf("Unable to decode feed JSON object: %v", err)
|
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
|
return &feed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeCategoryPayload(data io.Reader) (*model.Category, error) {
|
func decodeCategoryPayload(r io.ReadCloser) (*model.Category, error) {
|
||||||
var category model.Category
|
var category model.Category
|
||||||
|
|
||||||
decoder := json.NewDecoder(data)
|
decoder := json.NewDecoder(r)
|
||||||
|
defer r.Close()
|
||||||
if err := decoder.Decode(&category); err != nil {
|
if err := decoder.Decode(&category); err != nil {
|
||||||
return nil, fmt.Errorf("Unable to decode category JSON object: %v", err)
|
return nil, fmt.Errorf("Unable to decode category JSON object: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,29 +7,30 @@ package api
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/miniflux/miniflux/http/handler"
|
"github.com/miniflux/miniflux/http/response/json"
|
||||||
"github.com/miniflux/miniflux/reader/subscription"
|
"github.com/miniflux/miniflux/reader/subscription"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetSubscriptions is the API handler to find subscriptions.
|
// GetSubscriptions is the API handler to find subscriptions.
|
||||||
func (c *Controller) GetSubscriptions(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) GetSubscriptions(w http.ResponseWriter, r *http.Request) {
|
||||||
websiteURL, err := decodeURLPayload(request.Body())
|
websiteURL, err := decodeURLPayload(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptions, err := subscription.FindSubscriptions(websiteURL)
|
subscriptions, err := subscription.FindSubscriptions(websiteURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(errors.New("Unable to discover subscriptions"))
|
json.ServerError(w, errors.New("Unable to discover subscriptions"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if subscriptions == nil {
|
if subscriptions == nil {
|
||||||
response.JSON().NotFound(fmt.Errorf("No subscription found"))
|
json.NotFound(w, fmt.Errorf("No subscription found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().Standard(subscriptions)
|
json.OK(w, subscriptions)
|
||||||
}
|
}
|
||||||
|
|
99
api/user.go
99
api/user.go
|
@ -6,182 +6,191 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"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.
|
// CreateUser is the API handler to create a new user.
|
||||||
func (c *Controller) CreateUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := context.New(r)
|
||||||
if !ctx.IsAdminUser() {
|
if !ctx.IsAdminUser() {
|
||||||
response.JSON().Forbidden()
|
json.Forbidden(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := decodeUserPayload(request.Body())
|
user, err := decodeUserPayload(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := user.ValidateUserCreation(); err != nil {
|
if err := user.ValidateUserCreation(); err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.store.UserExists(user.Username) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.store.CreateUser(user)
|
err = c.store.CreateUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(errors.New("Unable to create this user"))
|
json.ServerError(w, errors.New("Unable to create this user"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Password = ""
|
user.Password = ""
|
||||||
response.JSON().Created(user)
|
json.Created(w, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUser is the API handler to update the given 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() {
|
if !ctx.IsAdminUser() {
|
||||||
response.JSON().Forbidden()
|
json.Forbidden(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, err := request.IntegerParam("userID")
|
userID, err := request.IntParam(r, "userID")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := decodeUserPayload(request.Body())
|
user, err := decodeUserPayload(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := user.ValidateUserModification(); err != nil {
|
if err := user.ValidateUserModification(); err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
originalUser, err := c.store.UserByID(userID)
|
originalUser, err := c.store.UserByID(userID)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if originalUser == nil {
|
if originalUser == nil {
|
||||||
response.JSON().NotFound(errors.New("User not found"))
|
json.NotFound(w, errors.New("User not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
originalUser.Merge(user)
|
originalUser.Merge(user)
|
||||||
if err = c.store.UpdateUser(originalUser); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().Created(originalUser)
|
json.Created(w, originalUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Users is the API handler to get the list of users.
|
// 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() {
|
if !ctx.IsAdminUser() {
|
||||||
response.JSON().Forbidden()
|
json.Forbidden(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := c.store.Users()
|
users, err := c.store.Users()
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
users.UseTimezone(ctx.UserTimezone())
|
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.
|
// 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() {
|
if !ctx.IsAdminUser() {
|
||||||
response.JSON().Forbidden()
|
json.Forbidden(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, err := request.IntegerParam("userID")
|
userID, err := request.IntParam(r, "userID")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := c.store.UserByID(userID)
|
user, err := c.store.UserByID(userID)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
response.JSON().NotFound(errors.New("User not found"))
|
json.NotFound(w, errors.New("User not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user.UseTimezone(ctx.UserTimezone())
|
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.
|
// 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() {
|
if !ctx.IsAdminUser() {
|
||||||
response.JSON().Forbidden()
|
json.Forbidden(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
username := request.StringParam("username", "")
|
username := request.Param(r, "username", "")
|
||||||
user, err := c.store.UserByUsername(username)
|
user, err := c.store.UserByUsername(username)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
response.JSON().NotFound(errors.New("User not found"))
|
json.NotFound(w, errors.New("User not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().Standard(user)
|
json.OK(w, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveUser is the API handler to remove an existing 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() {
|
if !ctx.IsAdminUser() {
|
||||||
response.JSON().Forbidden()
|
json.Forbidden(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, err := request.IntegerParam("userID")
|
userID, err := request.IntParam(r, "userID")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().BadRequest(err)
|
json.BadRequest(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := c.store.UserByID(userID)
|
user, err := c.store.UserByID(userID)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
response.JSON().NotFound(errors.New("User not found"))
|
json.NotFound(w, errors.New("User not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.store.RemoveUser(user.ID); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().NoContent()
|
json.NoContent(w)
|
||||||
}
|
}
|
||||||
|
|
183
daemon/routes.go
183
daemon/routes.go
|
@ -10,7 +10,6 @@ import (
|
||||||
"github.com/miniflux/miniflux/api"
|
"github.com/miniflux/miniflux/api"
|
||||||
"github.com/miniflux/miniflux/config"
|
"github.com/miniflux/miniflux/config"
|
||||||
"github.com/miniflux/miniflux/fever"
|
"github.com/miniflux/miniflux/fever"
|
||||||
"github.com/miniflux/miniflux/http/handler"
|
|
||||||
"github.com/miniflux/miniflux/locale"
|
"github.com/miniflux/miniflux/locale"
|
||||||
"github.com/miniflux/miniflux/middleware"
|
"github.com/miniflux/miniflux/middleware"
|
||||||
"github.com/miniflux/miniflux/reader/feed"
|
"github.com/miniflux/miniflux/reader/feed"
|
||||||
|
@ -25,14 +24,9 @@ import (
|
||||||
func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handler, pool *scheduler.WorkerPool, translator *locale.Translator) *mux.Router {
|
func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handler, pool *scheduler.WorkerPool, translator *locale.Translator) *mux.Router {
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
templateEngine := template.NewEngine(cfg, router, translator)
|
templateEngine := template.NewEngine(cfg, router, translator)
|
||||||
|
|
||||||
apiController := api.NewController(store, feedHandler)
|
apiController := api.NewController(store, feedHandler)
|
||||||
feverController := fever.NewController(store)
|
feverController := fever.NewController(store)
|
||||||
uiController := ui.NewController(cfg, store, pool, feedHandler)
|
uiController := ui.NewController(cfg, store, pool, feedHandler, templateEngine, translator, router)
|
||||||
|
|
||||||
apiHandler := handler.NewHandler(cfg, store, router, templateEngine, translator)
|
|
||||||
feverHandler := handler.NewHandler(cfg, store, router, templateEngine, translator)
|
|
||||||
uiHandler := handler.NewHandler(cfg, store, router, templateEngine, translator)
|
|
||||||
middleware := middleware.New(cfg, store, router)
|
middleware := middleware.New(cfg, store, router)
|
||||||
|
|
||||||
if cfg.BasePath() != "" {
|
if cfg.BasePath() != "" {
|
||||||
|
@ -41,6 +35,7 @@ func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handle
|
||||||
|
|
||||||
router.Use(middleware.HeaderConfig)
|
router.Use(middleware.HeaderConfig)
|
||||||
router.Use(middleware.Logging)
|
router.Use(middleware.Logging)
|
||||||
|
router.Use(middleware.CommonHeaders)
|
||||||
|
|
||||||
router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
|
router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte("OK"))
|
w.Write([]byte("OK"))
|
||||||
|
@ -53,100 +48,100 @@ func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handle
|
||||||
|
|
||||||
feverRouter := router.PathPrefix("/fever").Subrouter()
|
feverRouter := router.PathPrefix("/fever").Subrouter()
|
||||||
feverRouter.Use(middleware.FeverAuth)
|
feverRouter.Use(middleware.FeverAuth)
|
||||||
feverRouter.Handle("/", feverHandler.Use(feverController.Handler)).Name("feverEndpoint")
|
feverRouter.HandleFunc("/", feverController.Handler).Name("feverEndpoint")
|
||||||
|
|
||||||
apiRouter := router.PathPrefix("/v1").Subrouter()
|
apiRouter := router.PathPrefix("/v1").Subrouter()
|
||||||
apiRouter.Use(middleware.BasicAuth)
|
apiRouter.Use(middleware.BasicAuth)
|
||||||
apiRouter.Handle("/users", apiHandler.Use(apiController.CreateUser)).Methods("POST")
|
apiRouter.HandleFunc("/users", apiController.CreateUser).Methods("POST")
|
||||||
apiRouter.Handle("/users", apiHandler.Use(apiController.Users)).Methods("GET")
|
apiRouter.HandleFunc("/users", apiController.Users).Methods("GET")
|
||||||
apiRouter.Handle("/users/{userID:[0-9]+}", apiHandler.Use(apiController.UserByID)).Methods("GET")
|
apiRouter.HandleFunc("/users/{userID:[0-9]+}", apiController.UserByID).Methods("GET")
|
||||||
apiRouter.Handle("/users/{userID:[0-9]+}", apiHandler.Use(apiController.UpdateUser)).Methods("PUT")
|
apiRouter.HandleFunc("/users/{userID:[0-9]+}", apiController.UpdateUser).Methods("PUT")
|
||||||
apiRouter.Handle("/users/{userID:[0-9]+}", apiHandler.Use(apiController.RemoveUser)).Methods("DELETE")
|
apiRouter.HandleFunc("/users/{userID:[0-9]+}", apiController.RemoveUser).Methods("DELETE")
|
||||||
apiRouter.Handle("/users/{username}", apiHandler.Use(apiController.UserByUsername)).Methods("GET")
|
apiRouter.HandleFunc("/users/{username}", apiController.UserByUsername).Methods("GET")
|
||||||
apiRouter.Handle("/categories", apiHandler.Use(apiController.CreateCategory)).Methods("POST")
|
apiRouter.HandleFunc("/categories", apiController.CreateCategory).Methods("POST")
|
||||||
apiRouter.Handle("/categories", apiHandler.Use(apiController.GetCategories)).Methods("GET")
|
apiRouter.HandleFunc("/categories", apiController.GetCategories).Methods("GET")
|
||||||
apiRouter.Handle("/categories/{categoryID}", apiHandler.Use(apiController.UpdateCategory)).Methods("PUT")
|
apiRouter.HandleFunc("/categories/{categoryID}", apiController.UpdateCategory).Methods("PUT")
|
||||||
apiRouter.Handle("/categories/{categoryID}", apiHandler.Use(apiController.RemoveCategory)).Methods("DELETE")
|
apiRouter.HandleFunc("/categories/{categoryID}", apiController.RemoveCategory).Methods("DELETE")
|
||||||
apiRouter.Handle("/discover", apiHandler.Use(apiController.GetSubscriptions)).Methods("POST")
|
apiRouter.HandleFunc("/discover", apiController.GetSubscriptions).Methods("POST")
|
||||||
apiRouter.Handle("/feeds", apiHandler.Use(apiController.CreateFeed)).Methods("POST")
|
apiRouter.HandleFunc("/feeds", apiController.CreateFeed).Methods("POST")
|
||||||
apiRouter.Handle("/feeds", apiHandler.Use(apiController.GetFeeds)).Methods("Get")
|
apiRouter.HandleFunc("/feeds", apiController.GetFeeds).Methods("Get")
|
||||||
apiRouter.Handle("/feeds/{feedID}/refresh", apiHandler.Use(apiController.RefreshFeed)).Methods("PUT")
|
apiRouter.HandleFunc("/feeds/{feedID}/refresh", apiController.RefreshFeed).Methods("PUT")
|
||||||
apiRouter.Handle("/feeds/{feedID}", apiHandler.Use(apiController.GetFeed)).Methods("GET")
|
apiRouter.HandleFunc("/feeds/{feedID}", apiController.GetFeed).Methods("GET")
|
||||||
apiRouter.Handle("/feeds/{feedID}", apiHandler.Use(apiController.UpdateFeed)).Methods("PUT")
|
apiRouter.HandleFunc("/feeds/{feedID}", apiController.UpdateFeed).Methods("PUT")
|
||||||
apiRouter.Handle("/feeds/{feedID}", apiHandler.Use(apiController.RemoveFeed)).Methods("DELETE")
|
apiRouter.HandleFunc("/feeds/{feedID}", apiController.RemoveFeed).Methods("DELETE")
|
||||||
apiRouter.Handle("/feeds/{feedID}/icon", apiHandler.Use(apiController.FeedIcon)).Methods("GET")
|
apiRouter.HandleFunc("/feeds/{feedID}/icon", apiController.FeedIcon).Methods("GET")
|
||||||
apiRouter.Handle("/export", apiHandler.Use(apiController.Export)).Methods("GET")
|
apiRouter.HandleFunc("/export", apiController.Export).Methods("GET")
|
||||||
apiRouter.Handle("/feeds/{feedID}/entries", apiHandler.Use(apiController.GetFeedEntries)).Methods("GET")
|
apiRouter.HandleFunc("/feeds/{feedID}/entries", apiController.GetFeedEntries).Methods("GET")
|
||||||
apiRouter.Handle("/feeds/{feedID}/entries/{entryID}", apiHandler.Use(apiController.GetFeedEntry)).Methods("GET")
|
apiRouter.HandleFunc("/feeds/{feedID}/entries/{entryID}", apiController.GetFeedEntry).Methods("GET")
|
||||||
apiRouter.Handle("/entries", apiHandler.Use(apiController.GetEntries)).Methods("GET")
|
apiRouter.HandleFunc("/entries", apiController.GetEntries).Methods("GET")
|
||||||
apiRouter.Handle("/entries", apiHandler.Use(apiController.SetEntryStatus)).Methods("PUT")
|
apiRouter.HandleFunc("/entries", apiController.SetEntryStatus).Methods("PUT")
|
||||||
apiRouter.Handle("/entries/{entryID}", apiHandler.Use(apiController.GetEntry)).Methods("GET")
|
apiRouter.HandleFunc("/entries/{entryID}", apiController.GetEntry).Methods("GET")
|
||||||
apiRouter.Handle("/entries/{entryID}/bookmark", apiHandler.Use(apiController.ToggleBookmark)).Methods("PUT")
|
apiRouter.HandleFunc("/entries/{entryID}/bookmark", apiController.ToggleBookmark).Methods("PUT")
|
||||||
|
|
||||||
uiRouter := router.NewRoute().Subrouter()
|
uiRouter := router.NewRoute().Subrouter()
|
||||||
uiRouter.Use(middleware.AppSession)
|
uiRouter.Use(middleware.AppSession)
|
||||||
uiRouter.Use(middleware.UserSession)
|
uiRouter.Use(middleware.UserSession)
|
||||||
uiRouter.Handle("/stylesheets/{name}.css", uiHandler.Use(uiController.Stylesheet)).Name("stylesheet").Methods("GET")
|
uiRouter.HandleFunc("/stylesheets/{name}.css", uiController.Stylesheet).Name("stylesheet").Methods("GET")
|
||||||
uiRouter.Handle("/js", uiHandler.Use(uiController.Javascript)).Name("javascript").Methods("GET")
|
uiRouter.HandleFunc("/js", uiController.Javascript).Name("javascript").Methods("GET")
|
||||||
uiRouter.Handle("/favicon.ico", uiHandler.Use(uiController.Favicon)).Name("favicon").Methods("GET")
|
uiRouter.HandleFunc("/favicon.ico", uiController.Favicon).Name("favicon").Methods("GET")
|
||||||
uiRouter.Handle("/icon/{filename}", uiHandler.Use(uiController.AppIcon)).Name("appIcon").Methods("GET")
|
uiRouter.HandleFunc("/icon/{filename}", uiController.AppIcon).Name("appIcon").Methods("GET")
|
||||||
uiRouter.Handle("/manifest.json", uiHandler.Use(uiController.WebManifest)).Name("webManifest").Methods("GET")
|
uiRouter.HandleFunc("/manifest.json", uiController.WebManifest).Name("webManifest").Methods("GET")
|
||||||
uiRouter.Handle("/subscribe", uiHandler.Use(uiController.AddSubscription)).Name("addSubscription").Methods("GET")
|
uiRouter.HandleFunc("/subscribe", uiController.AddSubscription).Name("addSubscription").Methods("GET")
|
||||||
uiRouter.Handle("/subscribe", uiHandler.Use(uiController.SubmitSubscription)).Name("submitSubscription").Methods("POST")
|
uiRouter.HandleFunc("/subscribe", uiController.SubmitSubscription).Name("submitSubscription").Methods("POST")
|
||||||
uiRouter.Handle("/subscriptions", uiHandler.Use(uiController.ChooseSubscription)).Name("chooseSubscription").Methods("POST")
|
uiRouter.HandleFunc("/subscriptions", uiController.ChooseSubscription).Name("chooseSubscription").Methods("POST")
|
||||||
uiRouter.Handle("/mark-all-as-read", uiHandler.Use(uiController.MarkAllAsRead)).Name("markAllAsRead").Methods("GET")
|
uiRouter.HandleFunc("/mark-all-as-read", uiController.MarkAllAsRead).Name("markAllAsRead").Methods("GET")
|
||||||
uiRouter.Handle("/unread", uiHandler.Use(uiController.ShowUnreadPage)).Name("unread").Methods("GET")
|
uiRouter.HandleFunc("/unread", uiController.ShowUnreadPage).Name("unread").Methods("GET")
|
||||||
uiRouter.Handle("/history", uiHandler.Use(uiController.ShowHistoryPage)).Name("history").Methods("GET")
|
uiRouter.HandleFunc("/history", uiController.ShowHistoryPage).Name("history").Methods("GET")
|
||||||
uiRouter.Handle("/starred", uiHandler.Use(uiController.ShowStarredPage)).Name("starred").Methods("GET")
|
uiRouter.HandleFunc("/starred", uiController.ShowStarredPage).Name("starred").Methods("GET")
|
||||||
uiRouter.Handle("/feed/{feedID}/refresh", uiHandler.Use(uiController.RefreshFeed)).Name("refreshFeed").Methods("GET")
|
uiRouter.HandleFunc("/feed/{feedID}/refresh", uiController.RefreshFeed).Name("refreshFeed").Methods("GET")
|
||||||
uiRouter.Handle("/feed/{feedID}/edit", uiHandler.Use(uiController.EditFeed)).Name("editFeed").Methods("GET")
|
uiRouter.HandleFunc("/feed/{feedID}/edit", uiController.EditFeed).Name("editFeed").Methods("GET")
|
||||||
uiRouter.Handle("/feed/{feedID}/remove", uiHandler.Use(uiController.RemoveFeed)).Name("removeFeed").Methods("POST")
|
uiRouter.HandleFunc("/feed/{feedID}/remove", uiController.RemoveFeed).Name("removeFeed").Methods("POST")
|
||||||
uiRouter.Handle("/feed/{feedID}/update", uiHandler.Use(uiController.UpdateFeed)).Name("updateFeed").Methods("POST")
|
uiRouter.HandleFunc("/feed/{feedID}/update", uiController.UpdateFeed).Name("updateFeed").Methods("POST")
|
||||||
uiRouter.Handle("/feed/{feedID}/entries", uiHandler.Use(uiController.ShowFeedEntries)).Name("feedEntries").Methods("GET")
|
uiRouter.HandleFunc("/feed/{feedID}/entries", uiController.ShowFeedEntries).Name("feedEntries").Methods("GET")
|
||||||
uiRouter.Handle("/feeds", uiHandler.Use(uiController.ShowFeedsPage)).Name("feeds").Methods("GET")
|
uiRouter.HandleFunc("/feeds", uiController.ShowFeedsPage).Name("feeds").Methods("GET")
|
||||||
uiRouter.Handle("/feeds/refresh", uiHandler.Use(uiController.RefreshAllFeeds)).Name("refreshAllFeeds").Methods("GET")
|
uiRouter.HandleFunc("/feeds/refresh", uiController.RefreshAllFeeds).Name("refreshAllFeeds").Methods("GET")
|
||||||
uiRouter.Handle("/unread/entry/{entryID}", uiHandler.Use(uiController.ShowUnreadEntry)).Name("unreadEntry").Methods("GET")
|
uiRouter.HandleFunc("/unread/entry/{entryID}", uiController.ShowUnreadEntry).Name("unreadEntry").Methods("GET")
|
||||||
uiRouter.Handle("/history/entry/{entryID}", uiHandler.Use(uiController.ShowReadEntry)).Name("readEntry").Methods("GET")
|
uiRouter.HandleFunc("/history/entry/{entryID}", uiController.ShowReadEntry).Name("readEntry").Methods("GET")
|
||||||
uiRouter.Handle("/history/flush", uiHandler.Use(uiController.FlushHistory)).Name("flushHistory").Methods("GET")
|
uiRouter.HandleFunc("/history/flush", uiController.FlushHistory).Name("flushHistory").Methods("GET")
|
||||||
uiRouter.Handle("/feed/{feedID}/entry/{entryID}", uiHandler.Use(uiController.ShowFeedEntry)).Name("feedEntry").Methods("GET")
|
uiRouter.HandleFunc("/feed/{feedID}/entry/{entryID}", uiController.ShowFeedEntry).Name("feedEntry").Methods("GET")
|
||||||
uiRouter.Handle("/category/{categoryID}/entry/{entryID}", uiHandler.Use(uiController.ShowCategoryEntry)).Name("categoryEntry").Methods("GET")
|
uiRouter.HandleFunc("/category/{categoryID}/entry/{entryID}", uiController.ShowCategoryEntry).Name("categoryEntry").Methods("GET")
|
||||||
uiRouter.Handle("/starred/entry/{entryID}", uiHandler.Use(uiController.ShowStarredEntry)).Name("starredEntry").Methods("GET")
|
uiRouter.HandleFunc("/starred/entry/{entryID}", uiController.ShowStarredEntry).Name("starredEntry").Methods("GET")
|
||||||
uiRouter.Handle("/entry/status", uiHandler.Use(uiController.UpdateEntriesStatus)).Name("updateEntriesStatus").Methods("POST")
|
uiRouter.HandleFunc("/entry/status", uiController.UpdateEntriesStatus).Name("updateEntriesStatus").Methods("POST")
|
||||||
uiRouter.Handle("/entry/save/{entryID}", uiHandler.Use(uiController.SaveEntry)).Name("saveEntry").Methods("POST")
|
uiRouter.HandleFunc("/entry/save/{entryID}", uiController.SaveEntry).Name("saveEntry").Methods("POST")
|
||||||
uiRouter.Handle("/entry/download/{entryID}", uiHandler.Use(uiController.FetchContent)).Name("fetchContent").Methods("POST")
|
uiRouter.HandleFunc("/entry/download/{entryID}", uiController.FetchContent).Name("fetchContent").Methods("POST")
|
||||||
uiRouter.Handle("/entry/bookmark/{entryID}", uiHandler.Use(uiController.ToggleBookmark)).Name("toggleBookmark").Methods("POST")
|
uiRouter.HandleFunc("/entry/bookmark/{entryID}", uiController.ToggleBookmark).Name("toggleBookmark").Methods("POST")
|
||||||
uiRouter.Handle("/categories", uiHandler.Use(uiController.ShowCategories)).Name("categories").Methods("GET")
|
uiRouter.HandleFunc("/categories", uiController.CategoryList).Name("categories").Methods("GET")
|
||||||
uiRouter.Handle("/category/create", uiHandler.Use(uiController.CreateCategory)).Name("createCategory").Methods("GET")
|
uiRouter.HandleFunc("/category/create", uiController.CreateCategory).Name("createCategory").Methods("GET")
|
||||||
uiRouter.Handle("/category/save", uiHandler.Use(uiController.SaveCategory)).Name("saveCategory").Methods("POST")
|
uiRouter.HandleFunc("/category/save", uiController.SaveCategory).Name("saveCategory").Methods("POST")
|
||||||
uiRouter.Handle("/category/{categoryID}/entries", uiHandler.Use(uiController.ShowCategoryEntries)).Name("categoryEntries").Methods("GET")
|
uiRouter.HandleFunc("/category/{categoryID}/entries", uiController.CategoryEntries).Name("categoryEntries").Methods("GET")
|
||||||
uiRouter.Handle("/category/{categoryID}/edit", uiHandler.Use(uiController.EditCategory)).Name("editCategory").Methods("GET")
|
uiRouter.HandleFunc("/category/{categoryID}/edit", uiController.EditCategory).Name("editCategory").Methods("GET")
|
||||||
uiRouter.Handle("/category/{categoryID}/update", uiHandler.Use(uiController.UpdateCategory)).Name("updateCategory").Methods("POST")
|
uiRouter.HandleFunc("/category/{categoryID}/update", uiController.UpdateCategory).Name("updateCategory").Methods("POST")
|
||||||
uiRouter.Handle("/category/{categoryID}/remove", uiHandler.Use(uiController.RemoveCategory)).Name("removeCategory").Methods("POST")
|
uiRouter.HandleFunc("/category/{categoryID}/remove", uiController.RemoveCategory).Name("removeCategory").Methods("POST")
|
||||||
uiRouter.Handle("/feed/icon/{iconID}", uiHandler.Use(uiController.ShowIcon)).Name("icon").Methods("GET")
|
uiRouter.HandleFunc("/feed/icon/{iconID}", uiController.ShowIcon).Name("icon").Methods("GET")
|
||||||
uiRouter.Handle("/proxy/{encodedURL}", uiHandler.Use(uiController.ImageProxy)).Name("proxy").Methods("GET")
|
uiRouter.HandleFunc("/proxy/{encodedURL}", uiController.ImageProxy).Name("proxy").Methods("GET")
|
||||||
uiRouter.Handle("/users", uiHandler.Use(uiController.ShowUsers)).Name("users").Methods("GET")
|
uiRouter.HandleFunc("/users", uiController.ShowUsers).Name("users").Methods("GET")
|
||||||
uiRouter.Handle("/user/create", uiHandler.Use(uiController.CreateUser)).Name("createUser").Methods("GET")
|
uiRouter.HandleFunc("/user/create", uiController.CreateUser).Name("createUser").Methods("GET")
|
||||||
uiRouter.Handle("/user/save", uiHandler.Use(uiController.SaveUser)).Name("saveUser").Methods("POST")
|
uiRouter.HandleFunc("/user/save", uiController.SaveUser).Name("saveUser").Methods("POST")
|
||||||
uiRouter.Handle("/users/{userID}/edit", uiHandler.Use(uiController.EditUser)).Name("editUser").Methods("GET")
|
uiRouter.HandleFunc("/users/{userID}/edit", uiController.EditUser).Name("editUser").Methods("GET")
|
||||||
uiRouter.Handle("/users/{userID}/update", uiHandler.Use(uiController.UpdateUser)).Name("updateUser").Methods("POST")
|
uiRouter.HandleFunc("/users/{userID}/update", uiController.UpdateUser).Name("updateUser").Methods("POST")
|
||||||
uiRouter.Handle("/users/{userID}/remove", uiHandler.Use(uiController.RemoveUser)).Name("removeUser").Methods("POST")
|
uiRouter.HandleFunc("/users/{userID}/remove", uiController.RemoveUser).Name("removeUser").Methods("POST")
|
||||||
uiRouter.Handle("/about", uiHandler.Use(uiController.AboutPage)).Name("about").Methods("GET")
|
uiRouter.HandleFunc("/about", uiController.About).Name("about").Methods("GET")
|
||||||
uiRouter.Handle("/settings", uiHandler.Use(uiController.ShowSettings)).Name("settings").Methods("GET")
|
uiRouter.HandleFunc("/settings", uiController.ShowSettings).Name("settings").Methods("GET")
|
||||||
uiRouter.Handle("/settings", uiHandler.Use(uiController.UpdateSettings)).Name("updateSettings").Methods("POST")
|
uiRouter.HandleFunc("/settings", uiController.UpdateSettings).Name("updateSettings").Methods("POST")
|
||||||
uiRouter.Handle("/bookmarklet", uiHandler.Use(uiController.Bookmarklet)).Name("bookmarklet").Methods("GET")
|
uiRouter.HandleFunc("/bookmarklet", uiController.Bookmarklet).Name("bookmarklet").Methods("GET")
|
||||||
uiRouter.Handle("/integrations", uiHandler.Use(uiController.ShowIntegrations)).Name("integrations").Methods("GET")
|
uiRouter.HandleFunc("/integrations", uiController.ShowIntegrations).Name("integrations").Methods("GET")
|
||||||
uiRouter.Handle("/integration", uiHandler.Use(uiController.UpdateIntegration)).Name("updateIntegration").Methods("POST")
|
uiRouter.HandleFunc("/integration", uiController.UpdateIntegration).Name("updateIntegration").Methods("POST")
|
||||||
uiRouter.Handle("/sessions", uiHandler.Use(uiController.ShowSessions)).Name("sessions").Methods("GET")
|
uiRouter.HandleFunc("/sessions", uiController.ShowSessions).Name("sessions").Methods("GET")
|
||||||
uiRouter.Handle("/sessions/{sessionID}/remove", uiHandler.Use(uiController.RemoveSession)).Name("removeSession").Methods("POST")
|
uiRouter.HandleFunc("/sessions/{sessionID}/remove", uiController.RemoveSession).Name("removeSession").Methods("POST")
|
||||||
uiRouter.Handle("/export", uiHandler.Use(uiController.Export)).Name("export").Methods("GET")
|
uiRouter.HandleFunc("/export", uiController.Export).Name("export").Methods("GET")
|
||||||
uiRouter.Handle("/import", uiHandler.Use(uiController.Import)).Name("import").Methods("GET")
|
uiRouter.HandleFunc("/import", uiController.Import).Name("import").Methods("GET")
|
||||||
uiRouter.Handle("/upload", uiHandler.Use(uiController.UploadOPML)).Name("uploadOPML").Methods("POST")
|
uiRouter.HandleFunc("/upload", uiController.UploadOPML).Name("uploadOPML").Methods("POST")
|
||||||
uiRouter.Handle("/oauth2/{provider}/unlink", uiHandler.Use(uiController.OAuth2Unlink)).Name("oauth2Unlink").Methods("GET")
|
uiRouter.HandleFunc("/oauth2/{provider}/unlink", uiController.OAuth2Unlink).Name("oauth2Unlink").Methods("GET")
|
||||||
uiRouter.Handle("/oauth2/{provider}/redirect", uiHandler.Use(uiController.OAuth2Redirect)).Name("oauth2Redirect").Methods("GET")
|
uiRouter.HandleFunc("/oauth2/{provider}/redirect", uiController.OAuth2Redirect).Name("oauth2Redirect").Methods("GET")
|
||||||
uiRouter.Handle("/oauth2/{provider}/callback", uiHandler.Use(uiController.OAuth2Callback)).Name("oauth2Callback").Methods("GET")
|
uiRouter.HandleFunc("/oauth2/{provider}/callback", uiController.OAuth2Callback).Name("oauth2Callback").Methods("GET")
|
||||||
uiRouter.Handle("/login", uiHandler.Use(uiController.CheckLogin)).Name("checkLogin").Methods("POST")
|
uiRouter.HandleFunc("/login", uiController.CheckLogin).Name("checkLogin").Methods("POST")
|
||||||
uiRouter.Handle("/logout", uiHandler.Use(uiController.Logout)).Name("logout").Methods("GET")
|
uiRouter.HandleFunc("/logout", uiController.Logout).Name("logout").Methods("GET")
|
||||||
uiRouter.Handle("/", uiHandler.Use(uiController.ShowLoginPage)).Name("login").Methods("GET")
|
uiRouter.HandleFunc("/", uiController.ShowLoginPage).Name("login").Methods("GET")
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
141
fever/fever.go
141
fever/fever.go
|
@ -5,11 +5,14 @@
|
||||||
package fever
|
package fever
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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/integration"
|
"github.com/miniflux/miniflux/integration"
|
||||||
"github.com/miniflux/miniflux/logger"
|
"github.com/miniflux/miniflux/logger"
|
||||||
"github.com/miniflux/miniflux/model"
|
"github.com/miniflux/miniflux/model"
|
||||||
|
@ -129,28 +132,28 @@ type Controller struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler handles Fever API calls
|
// 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 {
|
switch {
|
||||||
case request.HasQueryParam("groups"):
|
case request.HasQueryParam(r, "groups"):
|
||||||
c.handleGroups(ctx, request, response)
|
c.handleGroups(w, r)
|
||||||
case request.HasQueryParam("feeds"):
|
case request.HasQueryParam(r, "feeds"):
|
||||||
c.handleFeeds(ctx, request, response)
|
c.handleFeeds(w, r)
|
||||||
case request.HasQueryParam("favicons"):
|
case request.HasQueryParam(r, "favicons"):
|
||||||
c.handleFavicons(ctx, request, response)
|
c.handleFavicons(w, r)
|
||||||
case request.HasQueryParam("unread_item_ids"):
|
case request.HasQueryParam(r, "unread_item_ids"):
|
||||||
c.handleUnreadItems(ctx, request, response)
|
c.handleUnreadItems(w, r)
|
||||||
case request.HasQueryParam("saved_item_ids"):
|
case request.HasQueryParam(r, "saved_item_ids"):
|
||||||
c.handleSavedItems(ctx, request, response)
|
c.handleSavedItems(w, r)
|
||||||
case request.HasQueryParam("items"):
|
case request.HasQueryParam(r, "items"):
|
||||||
c.handleItems(ctx, request, response)
|
c.handleItems(w, r)
|
||||||
case request.FormValue("mark") == "item":
|
case r.FormValue("mark") == "item":
|
||||||
c.handleWriteItems(ctx, request, response)
|
c.handleWriteItems(w, r)
|
||||||
case request.FormValue("mark") == "feed":
|
case r.FormValue("mark") == "feed":
|
||||||
c.handleWriteFeeds(ctx, request, response)
|
c.handleWriteFeeds(w, r)
|
||||||
case request.FormValue("mark") == "group":
|
case r.FormValue("mark") == "group":
|
||||||
c.handleWriteGroups(ctx, request, response)
|
c.handleWriteGroups(w, r)
|
||||||
default:
|
default:
|
||||||
response.JSON().Standard(newBaseResponse())
|
json.OK(w, newBaseResponse())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,19 +177,20 @@ The “Sparks” super group is not included in this response and is composed of
|
||||||
is_spark equal to 1.
|
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()
|
userID := ctx.UserID()
|
||||||
logger.Debug("[Fever] Fetching groups for userID=%d", userID)
|
logger.Debug("[Fever] Fetching groups for userID=%d", userID)
|
||||||
|
|
||||||
categories, err := c.store.Categories(userID)
|
categories, err := c.store.Categories(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(err)
|
json.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
feeds, err := c.store.Feeds(userID)
|
feeds, err := c.store.Feeds(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(err)
|
json.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +201,7 @@ func (c *Controller) handleGroups(ctx *handler.Context, request *handler.Request
|
||||||
|
|
||||||
result.FeedsGroups = c.buildFeedGroups(feeds)
|
result.FeedsGroups = c.buildFeedGroups(feeds)
|
||||||
result.SetCommonValues()
|
result.SetCommonValues()
|
||||||
response.JSON().Standard(result)
|
json.OK(w, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -224,13 +228,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.
|
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()
|
userID := ctx.UserID()
|
||||||
logger.Debug("[Fever] Fetching feeds for userID=%d", userID)
|
logger.Debug("[Fever] Fetching feeds for userID=%d", userID)
|
||||||
|
|
||||||
feeds, err := c.store.Feeds(userID)
|
feeds, err := c.store.Feeds(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(err)
|
json.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,7 +260,7 @@ func (c *Controller) handleFeeds(ctx *handler.Context, request *handler.Request,
|
||||||
|
|
||||||
result.FeedsGroups = c.buildFeedGroups(feeds)
|
result.FeedsGroups = c.buildFeedGroups(feeds)
|
||||||
result.SetCommonValues()
|
result.SetCommonValues()
|
||||||
response.JSON().Standard(result)
|
json.OK(w, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -277,13 +282,14 @@ A PHP/HTML example:
|
||||||
|
|
||||||
echo '<img src="data:'.$favicon['data'].'">';
|
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()
|
userID := ctx.UserID()
|
||||||
logger.Debug("[Fever] Fetching favicons for userID=%d", userID)
|
logger.Debug("[Fever] Fetching favicons for userID=%d", userID)
|
||||||
|
|
||||||
icons, err := c.store.Icons(userID)
|
icons, err := c.store.Icons(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(err)
|
json.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,7 +302,7 @@ func (c *Controller) handleFavicons(ctx *handler.Context, request *handler.Reque
|
||||||
}
|
}
|
||||||
|
|
||||||
result.SetCommonValues()
|
result.SetCommonValues()
|
||||||
response.JSON().Standard(result)
|
json.OK(w, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -330,9 +336,10 @@ Three optional arguments control determine the items included in the response.
|
||||||
(added in API version 2)
|
(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
|
var result itemsResponse
|
||||||
|
|
||||||
|
ctx := context.New(r)
|
||||||
userID := ctx.UserID()
|
userID := ctx.UserID()
|
||||||
logger.Debug("[Fever] Fetching items for userID=%d", userID)
|
logger.Debug("[Fever] Fetching items for userID=%d", userID)
|
||||||
|
|
||||||
|
@ -342,17 +349,17 @@ func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request,
|
||||||
builder.WithOrder("id")
|
builder.WithOrder("id")
|
||||||
builder.WithDirection(model.DefaultSortingDirection)
|
builder.WithDirection(model.DefaultSortingDirection)
|
||||||
|
|
||||||
sinceID := request.QueryIntegerParam("since_id", 0)
|
sinceID := request.QueryIntParam(r, "since_id", 0)
|
||||||
if sinceID > 0 {
|
if sinceID > 0 {
|
||||||
builder.WithGreaterThanEntryID(int64(sinceID))
|
builder.WithGreaterThanEntryID(int64(sinceID))
|
||||||
}
|
}
|
||||||
|
|
||||||
maxID := request.QueryIntegerParam("max_id", 0)
|
maxID := request.QueryIntParam(r, "max_id", 0)
|
||||||
if maxID > 0 {
|
if maxID > 0 {
|
||||||
builder.WithOffset(maxID)
|
builder.WithOffset(maxID)
|
||||||
}
|
}
|
||||||
|
|
||||||
csvItemIDs := request.QueryStringParam("with_ids", "")
|
csvItemIDs := request.QueryParam(r, "with_ids", "")
|
||||||
if csvItemIDs != "" {
|
if csvItemIDs != "" {
|
||||||
var itemIDs []int64
|
var itemIDs []int64
|
||||||
|
|
||||||
|
@ -367,7 +374,7 @@ func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request,
|
||||||
|
|
||||||
entries, err := builder.GetEntries()
|
entries, err := builder.GetEntries()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(err)
|
json.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -375,7 +382,7 @@ func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request,
|
||||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||||
result.Total, err = builder.CountEntries()
|
result.Total, err = builder.CountEntries()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(err)
|
json.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,7 +411,7 @@ func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request,
|
||||||
}
|
}
|
||||||
|
|
||||||
result.SetCommonValues()
|
result.SetCommonValues()
|
||||||
response.JSON().Standard(result)
|
json.OK(w, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -414,7 +421,8 @@ with the remote Fever installation.
|
||||||
A request with the unread_item_ids argument will return one additional member:
|
A request with the unread_item_ids argument will return one additional member:
|
||||||
unread_item_ids (string/comma-separated list of positive integers)
|
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()
|
userID := ctx.UserID()
|
||||||
logger.Debug("[Fever] Fetching unread items for userID=%d", userID)
|
logger.Debug("[Fever] Fetching unread items for userID=%d", userID)
|
||||||
|
|
||||||
|
@ -422,7 +430,7 @@ func (c *Controller) handleUnreadItems(ctx *handler.Context, request *handler.Re
|
||||||
builder.WithStatus(model.EntryStatusUnread)
|
builder.WithStatus(model.EntryStatusUnread)
|
||||||
entries, err := builder.GetEntries()
|
entries, err := builder.GetEntries()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(err)
|
json.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -434,7 +442,7 @@ func (c *Controller) handleUnreadItems(ctx *handler.Context, request *handler.Re
|
||||||
var result unreadResponse
|
var result unreadResponse
|
||||||
result.ItemIDs = strings.Join(itemIDs, ",")
|
result.ItemIDs = strings.Join(itemIDs, ",")
|
||||||
result.SetCommonValues()
|
result.SetCommonValues()
|
||||||
response.JSON().Standard(result)
|
json.OK(w, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -445,7 +453,8 @@ with the remote Fever installation.
|
||||||
|
|
||||||
saved_item_ids (string/comma-separated list of positive integers)
|
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()
|
userID := ctx.UserID()
|
||||||
logger.Debug("[Fever] Fetching saved items for userID=%d", userID)
|
logger.Debug("[Fever] Fetching saved items for userID=%d", userID)
|
||||||
|
|
||||||
|
@ -454,7 +463,7 @@ func (c *Controller) handleSavedItems(ctx *handler.Context, request *handler.Req
|
||||||
|
|
||||||
entryIDs, err := builder.GetEntryIDs()
|
entryIDs, err := builder.GetEntryIDs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(err)
|
json.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -465,7 +474,7 @@ func (c *Controller) handleSavedItems(ctx *handler.Context, request *handler.Req
|
||||||
|
|
||||||
result := &savedResponse{ItemIDs: strings.Join(itemsIDs, ",")}
|
result := &savedResponse{ItemIDs: strings.Join(itemsIDs, ",")}
|
||||||
result.SetCommonValues()
|
result.SetCommonValues()
|
||||||
response.JSON().Standard(result)
|
json.OK(w, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -473,11 +482,12 @@ func (c *Controller) handleSavedItems(ctx *handler.Context, request *handler.Req
|
||||||
as=? where ? is replaced with read, saved or unsaved
|
as=? where ? is replaced with read, saved or unsaved
|
||||||
id=? where ? is replaced with the id of the item to modify
|
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()
|
userID := ctx.UserID()
|
||||||
logger.Debug("[Fever] Receiving mark=item call for userID=%d", userID)
|
logger.Debug("[Fever] Receiving mark=item call for userID=%d", userID)
|
||||||
|
|
||||||
entryID := request.FormIntegerValue("id")
|
entryID := request.FormIntValue(r, "id")
|
||||||
if entryID <= 0 {
|
if entryID <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -488,7 +498,7 @@ func (c *Controller) handleWriteItems(ctx *handler.Context, request *handler.Req
|
||||||
|
|
||||||
entry, err := builder.GetEntry()
|
entry, err := builder.GetEntry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(err)
|
json.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -496,20 +506,23 @@ func (c *Controller) handleWriteItems(ctx *handler.Context, request *handler.Req
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch request.FormValue("as") {
|
switch r.FormValue("as") {
|
||||||
case "read":
|
case "read":
|
||||||
|
logger.Debug("[Fever] Mark entry #%d as read", entryID)
|
||||||
c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead)
|
c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead)
|
||||||
case "unread":
|
case "unread":
|
||||||
|
logger.Debug("[Fever] Mark entry #%d as unread", entryID)
|
||||||
c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
|
c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
|
||||||
case "saved", "unsaved":
|
case "saved", "unsaved":
|
||||||
|
logger.Debug("[Fever] Mark entry #%d as saved/unsaved", entryID)
|
||||||
if err := c.store.ToggleBookmark(userID, entryID); err != nil {
|
if err := c.store.ToggleBookmark(userID, entryID); err != nil {
|
||||||
response.JSON().ServerError(err)
|
json.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
settings, err := c.store.Integration(userID)
|
settings, err := c.store.Integration(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(err)
|
json.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -518,7 +531,7 @@ func (c *Controller) handleWriteItems(ctx *handler.Context, request *handler.Req
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().Standard(newBaseResponse())
|
json.OK(w, newBaseResponse())
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -527,11 +540,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
|
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
|
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()
|
userID := ctx.UserID()
|
||||||
logger.Debug("[Fever] Receiving mark=feed call for userID=%d", userID)
|
logger.Debug("[Fever] Receiving mark=feed call for userID=%d", userID)
|
||||||
|
|
||||||
feedID := request.FormIntegerValue("id")
|
feedID := request.FormIntValue(r, "id")
|
||||||
if feedID <= 0 {
|
if feedID <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -540,7 +554,7 @@ func (c *Controller) handleWriteFeeds(ctx *handler.Context, request *handler.Req
|
||||||
builder.WithStatus(model.EntryStatusUnread)
|
builder.WithStatus(model.EntryStatusUnread)
|
||||||
builder.WithFeedID(feedID)
|
builder.WithFeedID(feedID)
|
||||||
|
|
||||||
before := request.FormIntegerValue("before")
|
before := request.FormIntValue(r, "before")
|
||||||
if before > 0 {
|
if before > 0 {
|
||||||
t := time.Unix(before, 0)
|
t := time.Unix(before, 0)
|
||||||
builder.Before(&t)
|
builder.Before(&t)
|
||||||
|
@ -548,17 +562,17 @@ func (c *Controller) handleWriteFeeds(ctx *handler.Context, request *handler.Req
|
||||||
|
|
||||||
entryIDs, err := builder.GetEntryIDs()
|
entryIDs, err := builder.GetEntryIDs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(err)
|
json.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead)
|
err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(err)
|
json.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().Standard(newBaseResponse())
|
json.OK(w, newBaseResponse())
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -567,11 +581,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
|
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
|
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()
|
userID := ctx.UserID()
|
||||||
logger.Debug("[Fever] Receiving mark=group call for userID=%d", userID)
|
logger.Debug("[Fever] Receiving mark=group call for userID=%d", userID)
|
||||||
|
|
||||||
groupID := request.FormIntegerValue("id")
|
groupID := request.FormIntValue(r, "id")
|
||||||
if groupID < 0 {
|
if groupID < 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -580,7 +595,7 @@ func (c *Controller) handleWriteGroups(ctx *handler.Context, request *handler.Re
|
||||||
builder.WithStatus(model.EntryStatusUnread)
|
builder.WithStatus(model.EntryStatusUnread)
|
||||||
builder.WithCategoryID(groupID)
|
builder.WithCategoryID(groupID)
|
||||||
|
|
||||||
before := request.FormIntegerValue("before")
|
before := request.FormIntValue(r, "before")
|
||||||
if before > 0 {
|
if before > 0 {
|
||||||
t := time.Unix(before, 0)
|
t := time.Unix(before, 0)
|
||||||
builder.Before(&t)
|
builder.Before(&t)
|
||||||
|
@ -588,17 +603,17 @@ func (c *Controller) handleWriteGroups(ctx *handler.Context, request *handler.Re
|
||||||
|
|
||||||
entryIDs, err := builder.GetEntryIDs()
|
entryIDs, err := builder.GetEntryIDs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(err)
|
json.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead)
|
err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.JSON().ServerError(err)
|
json.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON().Standard(newBaseResponse())
|
json.OK(w, newBaseResponse())
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
108
http/context/context.go
Normal file
108
http/context/context.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
}
|
|
@ -1,167 +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/route"
|
|
||||||
"github.com/miniflux/miniflux/locale"
|
|
||||||
"github.com/miniflux/miniflux/logger"
|
|
||||||
"github.com/miniflux/miniflux/middleware"
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|
||||||
language := c.getContextStringValue(middleware.UserLanguageContextKey)
|
|
||||||
if language == "" {
|
|
||||||
language = "en_US"
|
|
||||||
}
|
|
||||||
return language
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,54 +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/locale"
|
|
||||||
"github.com/miniflux/miniflux/storage"
|
|
||||||
"github.com/miniflux/miniflux/template"
|
|
||||||
"github.com/miniflux/miniflux/timer"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ControllerFunc is an application HTTP handler.
|
|
||||||
type ControllerFunc func(ctx *Context, request *Request, response *Response)
|
|
||||||
|
|
||||||
// Handler manages HTTP handlers.
|
|
||||||
type Handler struct {
|
|
||||||
cfg *config.Config
|
|
||||||
store *storage.Storage
|
|
||||||
translator *locale.Translator
|
|
||||||
template *template.Engine
|
|
||||||
router *mux.Router
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use is a wrapper around an HTTP handler.
|
|
||||||
func (h *Handler) Use(f ControllerFunc) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
defer timer.ExecutionTime(time.Now(), r.URL.Path)
|
|
||||||
|
|
||||||
ctx := NewContext(r, h.store, h.router, h.translator)
|
|
||||||
request := NewRequest(r)
|
|
||||||
response := NewResponse(h.cfg, w, r, h.template)
|
|
||||||
|
|
||||||
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) *Handler {
|
|
||||||
return &Handler{
|
|
||||||
cfg: cfg,
|
|
||||||
store: store,
|
|
||||||
translator: translator,
|
|
||||||
router: router,
|
|
||||||
template: template,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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, language string, args map[string]interface{}) {
|
|
||||||
h.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
h.template.Render(h.writer, template, language, 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))
|
|
||||||
}
|
|
90
http/request/request.go
Normal file
90
http/request/request.go
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
// 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/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"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
|
||||||
|
}
|
57
http/response/html/html.go
Normal file
57
http/response/html/html.go
Normal file
|
@ -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"))
|
||||||
|
}
|
107
http/response/json/json.go
Normal file
107
http/response/json/json.go
Normal file
|
@ -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
|
||||||
|
}
|
34
http/response/response.go
Normal file
34
http/response/response.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
23
http/response/xml/xml.go
Normal file
23
http/response/xml/xml.go
Normal file
|
@ -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))
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
// Code generated by go generate; DO NOT EDIT.
|
||||||
// 2018-04-09 20:38:50.319066697 -0700 PDT m=+0.026775461
|
// 2018-04-29 16:22:00.540830112 -0700 PDT m=+0.025120206
|
||||||
|
|
||||||
package locale
|
package locale
|
||||||
|
|
||||||
|
@ -161,7 +161,7 @@ var translations = map[string]string{
|
||||||
"Scraper Rules": "Extraktionsregeln",
|
"Scraper Rules": "Extraktionsregeln",
|
||||||
"Rewrite Rules": "Umschreiberegeln",
|
"Rewrite Rules": "Umschreiberegeln",
|
||||||
"Preferences saved!": "Einstellungen gespeichert!",
|
"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",
|
"Save articles to Wallabag": "Artikel in Wallabag speichern",
|
||||||
"Wallabag API Endpoint": "Wallabag URL",
|
"Wallabag API Endpoint": "Wallabag URL",
|
||||||
"Wallabag Client ID": "Wallabag Client-ID",
|
"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",
|
"Scraper Rules": "Règles pour récupérer le contenu original",
|
||||||
"Rewrite Rules": "Règles de réécriture",
|
"Rewrite Rules": "Règles de réécriture",
|
||||||
"Preferences saved!": "Préférences sauvegardées !",
|
"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",
|
"Save articles to Wallabag": "Sauvegarder les articles vers Wallabag",
|
||||||
"Wallabag API Endpoint": "URL de l'API de Wallabag",
|
"Wallabag API Endpoint": "URL de l'API de Wallabag",
|
||||||
"Wallabag Client ID": "Identifiant du client Wallabag",
|
"Wallabag Client ID": "Identifiant du client Wallabag",
|
||||||
|
@ -459,7 +459,9 @@ 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)",
|
"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",
|
"Website unreachable, the request timed out after %d seconds": "Site web injoignable, la requête à échouée après %d secondes",
|
||||||
"Comments": "Commentaires",
|
"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é !"
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
"nl_NL": `{
|
"nl_NL": `{
|
||||||
|
@ -618,7 +620,7 @@ var translations = map[string]string{
|
||||||
"Scraper Rules": "Scraper regels",
|
"Scraper Rules": "Scraper regels",
|
||||||
"Rewrite Rules": "Rewrite regels",
|
"Rewrite Rules": "Rewrite regels",
|
||||||
"Preferences saved!": "Instellingen opgeslagen!",
|
"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",
|
"Save articles to Wallabag": "Sauvegarder les articles vers Wallabag",
|
||||||
"Wallabag API Endpoint": "Wallabag URL",
|
"Wallabag API Endpoint": "Wallabag URL",
|
||||||
"Wallabag Client ID": "Wallabag Client-ID",
|
"Wallabag Client ID": "Wallabag Client-ID",
|
||||||
|
@ -842,7 +844,7 @@ var translations = map[string]string{
|
||||||
"Scraper Rules": "Zasady ekstrakcji",
|
"Scraper Rules": "Zasady ekstrakcji",
|
||||||
"Rewrite Rules": "Reguły zapisu",
|
"Rewrite Rules": "Reguły zapisu",
|
||||||
"Preferences saved!": "Ustawienia zapisane!",
|
"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",
|
"Save articles to Wallabag": "Zapisz artykuły do Wallabag",
|
||||||
"Wallabag API Endpoint": "Wallabag URL",
|
"Wallabag API Endpoint": "Wallabag URL",
|
||||||
"Wallabag Client ID": "Wallabag Client-ID",
|
"Wallabag Client ID": "Wallabag Client-ID",
|
||||||
|
@ -1064,7 +1066,7 @@ var translations = map[string]string{
|
||||||
"Scraper Rules": "Scraper规则",
|
"Scraper Rules": "Scraper规则",
|
||||||
"Rewrite Rules": "重写规则",
|
"Rewrite Rules": "重写规则",
|
||||||
"Preferences saved!": "偏好已存储!",
|
"Preferences saved!": "偏好已存储!",
|
||||||
"Your external account is now linked !": "您的外部账号已关联!",
|
"Your external account is now linked!": "您的外部账号已关联!",
|
||||||
"Save articles to Wallabag": "保存文章到Wallabag",
|
"Save articles to Wallabag": "保存文章到Wallabag",
|
||||||
"Wallabag API Endpoint": "Wallabag URL",
|
"Wallabag API Endpoint": "Wallabag URL",
|
||||||
"Wallabag Client ID": "Wallabag 客户端ID",
|
"Wallabag Client ID": "Wallabag 客户端ID",
|
||||||
|
@ -1132,10 +1134,10 @@ var translations = map[string]string{
|
||||||
}
|
}
|
||||||
|
|
||||||
var translationsChecksums = map[string]string{
|
var translationsChecksums = map[string]string{
|
||||||
"de_DE": "df47fc009e6a021579c7e004ebc0b00eae7bf47c23daaf74489fb4c15881296f",
|
"de_DE": "791d72c96137ab03b729017bdfa27c8eed2f65912e372fcb5b2796d5099d5498",
|
||||||
"en_US": "6fe95384260941e8a5a3c695a655a932e0a8a6a572c1e45cb2b1ae8baa01b897",
|
"en_US": "6fe95384260941e8a5a3c695a655a932e0a8a6a572c1e45cb2b1ae8baa01b897",
|
||||||
"fr_FR": "fb572aee29b90fcaa866e97d2e1cab8c6b8cd8a1cb76c7e83d7aa778748dd283",
|
"fr_FR": "b4e407e3665b30b29da3bce197a035b842a9bdd7781c28cc056d978b46646f3c",
|
||||||
"nl_NL": "d427d6a5e843be576040dee004df2b685a839a38b2e5f06435faa2973f1f4c70",
|
"nl_NL": "1a73f1dd1c4c0d2c2adc8695cdd050c2dad81c14876caed3892b44adc2491265",
|
||||||
"pl_PL": "4dcf7c3f44c80ca81ecdbef96bdb21d1ae1a8a6caf60cc11403e5e041efc5ca9",
|
"pl_PL": "da709c14ff71f3b516eec66cb2758d89c5feab1472c94b2b518f425162a9f806",
|
||||||
"zh_CN": "bfa05d3b3396df6222414a3a6949b73b486cd021499ecd3a34ce8e04e93aad93",
|
"zh_CN": "d80594c1b67d15e9f4673d3d62fe4949e8606a5fdfb741d8a8921f21dceb8cf2",
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,7 +155,7 @@
|
||||||
"Scraper Rules": "Extraktionsregeln",
|
"Scraper Rules": "Extraktionsregeln",
|
||||||
"Rewrite Rules": "Umschreiberegeln",
|
"Rewrite Rules": "Umschreiberegeln",
|
||||||
"Preferences saved!": "Einstellungen gespeichert!",
|
"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",
|
"Save articles to Wallabag": "Artikel in Wallabag speichern",
|
||||||
"Wallabag API Endpoint": "Wallabag URL",
|
"Wallabag API Endpoint": "Wallabag URL",
|
||||||
"Wallabag Client ID": "Wallabag Client-ID",
|
"Wallabag Client ID": "Wallabag Client-ID",
|
||||||
|
|
|
@ -155,7 +155,7 @@
|
||||||
"Scraper Rules": "Règles pour récupérer le contenu original",
|
"Scraper Rules": "Règles pour récupérer le contenu original",
|
||||||
"Rewrite Rules": "Règles de réécriture",
|
"Rewrite Rules": "Règles de réécriture",
|
||||||
"Preferences saved!": "Préférences sauvegardées !",
|
"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",
|
"Save articles to Wallabag": "Sauvegarder les articles vers Wallabag",
|
||||||
"Wallabag API Endpoint": "URL de l'API de Wallabag",
|
"Wallabag API Endpoint": "URL de l'API de Wallabag",
|
||||||
"Wallabag Client ID": "Identifiant du client Wallabag",
|
"Wallabag Client ID": "Identifiant du client Wallabag",
|
||||||
|
@ -219,5 +219,7 @@
|
||||||
"This website is permanently unreachable (original error: %q)": "Ce site web n'est pas joignable de façon permanente (erreur originale : %q)",
|
"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",
|
"Website unreachable, the request timed out after %d seconds": "Site web injoignable, la requête à échouée après %d secondes",
|
||||||
"Comments": "Commentaires",
|
"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é !"
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,7 +154,7 @@
|
||||||
"Scraper Rules": "Scraper regels",
|
"Scraper Rules": "Scraper regels",
|
||||||
"Rewrite Rules": "Rewrite regels",
|
"Rewrite Rules": "Rewrite regels",
|
||||||
"Preferences saved!": "Instellingen opgeslagen!",
|
"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",
|
"Save articles to Wallabag": "Sauvegarder les articles vers Wallabag",
|
||||||
"Wallabag API Endpoint": "Wallabag URL",
|
"Wallabag API Endpoint": "Wallabag URL",
|
||||||
"Wallabag Client ID": "Wallabag Client-ID",
|
"Wallabag Client ID": "Wallabag Client-ID",
|
||||||
|
|
|
@ -157,7 +157,7 @@
|
||||||
"Scraper Rules": "Zasady ekstrakcji",
|
"Scraper Rules": "Zasady ekstrakcji",
|
||||||
"Rewrite Rules": "Reguły zapisu",
|
"Rewrite Rules": "Reguły zapisu",
|
||||||
"Preferences saved!": "Ustawienia zapisane!",
|
"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",
|
"Save articles to Wallabag": "Zapisz artykuły do Wallabag",
|
||||||
"Wallabag API Endpoint": "Wallabag URL",
|
"Wallabag API Endpoint": "Wallabag URL",
|
||||||
"Wallabag Client ID": "Wallabag Client-ID",
|
"Wallabag Client ID": "Wallabag Client-ID",
|
||||||
|
|
|
@ -155,7 +155,7 @@
|
||||||
"Scraper Rules": "Scraper规则",
|
"Scraper Rules": "Scraper规则",
|
||||||
"Rewrite Rules": "重写规则",
|
"Rewrite Rules": "重写规则",
|
||||||
"Preferences saved!": "偏好已存储!",
|
"Preferences saved!": "偏好已存储!",
|
||||||
"Your external account is now linked !": "您的外部账号已关联!",
|
"Your external account is now linked!": "您的外部账号已关联!",
|
||||||
"Save articles to Wallabag": "保存文章到Wallabag",
|
"Save articles to Wallabag": "保存文章到Wallabag",
|
||||||
"Wallabag API Endpoint": "Wallabag URL",
|
"Wallabag API Endpoint": "Wallabag URL",
|
||||||
"Wallabag Client ID": "Wallabag 客户端ID",
|
"Wallabag Client ID": "Wallabag 客户端ID",
|
||||||
|
|
|
@ -6,9 +6,12 @@ package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/miniflux/miniflux/http/cookie"
|
"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/logger"
|
||||||
"github.com/miniflux/miniflux/model"
|
"github.com/miniflux/miniflux/model"
|
||||||
)
|
)
|
||||||
|
@ -17,20 +20,21 @@ import (
|
||||||
func (m *Middleware) AppSession(next http.Handler) http.Handler {
|
func (m *Middleware) AppSession(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var err error
|
var err error
|
||||||
session := m.getSessionValueFromCookie(r)
|
session := m.getAppSessionValueFromCookie(r)
|
||||||
|
|
||||||
if session == nil {
|
if session == nil {
|
||||||
logger.Debug("[Middleware:Session] Session not found")
|
logger.Debug("[Middleware:AppSession] Session not found")
|
||||||
|
|
||||||
session, err = m.store.CreateSession()
|
session, err = m.store.CreateSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("[Middleware:Session] %v", err)
|
logger.Error("[Middleware:AppSession] %v", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
html.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.SetCookie(w, cookie.New(cookie.CookieSessionID, session.ID, m.cfg.IsHTTPS, m.cfg.BasePath()))
|
http.SetCookie(w, cookie.New(cookie.CookieSessionID, session.ID, m.cfg.IsHTTPS, m.cfg.BasePath()))
|
||||||
} else {
|
} else {
|
||||||
logger.Debug("[Middleware:Session] %s", session)
|
logger.Debug("[Middleware:AppSession] %s", session)
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Method == "POST" {
|
if r.Method == "POST" {
|
||||||
|
@ -38,9 +42,8 @@ func (m *Middleware) AppSession(next http.Handler) http.Handler {
|
||||||
headerValue := r.Header.Get("X-Csrf-Token")
|
headerValue := r.Header.Get("X-Csrf-Token")
|
||||||
|
|
||||||
if session.Data.CSRF != formValue && session.Data.CSRF != headerValue {
|
if session.Data.CSRF != formValue && session.Data.CSRF != headerValue {
|
||||||
logger.Error(`[Middleware:Session] Invalid or missing CSRF token: Form="%s", Header="%s"`, formValue, headerValue)
|
logger.Error(`[Middleware:AppSession] Invalid or missing CSRF token: Form="%s", Header="%s"`, formValue, headerValue)
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
html.BadRequest(w, errors.New("invalid or missing CSRF"))
|
||||||
w.Write([]byte("Invalid or missing CSRF session!"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,15 +59,15 @@ func (m *Middleware) AppSession(next http.Handler) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Middleware) getSessionValueFromCookie(r *http.Request) *model.Session {
|
func (m *Middleware) getAppSessionValueFromCookie(r *http.Request) *model.Session {
|
||||||
sessionCookie, err := r.Cookie(cookie.CookieSessionID)
|
cookieValue := request.Cookie(r, cookie.CookieSessionID)
|
||||||
if err == http.ErrNoCookie {
|
if cookieValue == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := m.store.Session(sessionCookie.Value)
|
session, err := m.store.Session(cookieValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("[Middleware:Session] %v", err)
|
logger.Error("[Middleware:AppSession] %v", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/miniflux/miniflux/http/response/json"
|
||||||
"github.com/miniflux/miniflux/logger"
|
"github.com/miniflux/miniflux/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,35 +16,30 @@ import (
|
||||||
func (m *Middleware) BasicAuth(next http.Handler) http.Handler {
|
func (m *Middleware) BasicAuth(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||||
errorResponse := `{"error_message": "Not Authorized"}`
|
|
||||||
|
|
||||||
username, password, authOK := r.BasicAuth()
|
username, password, authOK := r.BasicAuth()
|
||||||
if !authOK {
|
if !authOK {
|
||||||
logger.Debug("[Middleware:BasicAuth] No authentication headers sent")
|
logger.Debug("[Middleware:BasicAuth] No authentication headers sent")
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
json.Unauthorized(w)
|
||||||
w.Write([]byte(errorResponse))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.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)
|
logger.Info("[Middleware:BasicAuth] Invalid username or password: %s", username)
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
json.Unauthorized(w)
|
||||||
w.Write([]byte(errorResponse))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := m.store.UserByUsername(username)
|
user, err := m.store.UserByUsername(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("[Middleware:BasicAuth] %v", err)
|
logger.Error("[Middleware:BasicAuth] %v", err)
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
json.ServerError(w, err)
|
||||||
w.Write([]byte(errorResponse))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
logger.Info("[Middleware:BasicAuth] User not found: %s", username)
|
logger.Info("[Middleware:BasicAuth] User not found: %s", username)
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
json.Unauthorized(w)
|
||||||
w.Write([]byte(errorResponse))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
25
middleware/common_headers.go
Normal file
25
middleware/common_headers.go
Normal file
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -8,27 +8,25 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/miniflux/miniflux/http/response/json"
|
||||||
"github.com/miniflux/miniflux/logger"
|
"github.com/miniflux/miniflux/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FeverAuth handles Fever API authentication.
|
// FeverAuth handles Fever API authentication.
|
||||||
func (m *Middleware) FeverAuth(next http.Handler) http.Handler {
|
func (m *Middleware) FeverAuth(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
logger.Debug("[Middleware:Fever]")
|
|
||||||
|
|
||||||
apiKey := r.FormValue("api_key")
|
apiKey := r.FormValue("api_key")
|
||||||
|
|
||||||
user, err := m.store.UserByFeverToken(apiKey)
|
user, err := m.store.UserByFeverToken(apiKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("[Fever] %v", err)
|
logger.Error("[Middleware:Fever] %v", err)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
json.OK(w, map[string]int{"api_version": 3, "auth": 0})
|
||||||
w.Write([]byte(`{"api_version": 3, "auth": 0}`))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
logger.Info("[Middleware:Fever] Fever authentication failure")
|
logger.Info("[Middleware:Fever] Fever authentication failure")
|
||||||
w.Header().Set("Content-Type", "application/json")
|
json.OK(w, map[string]int{"api_version": 3, "auth": 0})
|
||||||
w.Write([]byte(`{"api_version": 3, "auth": 0}`))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/miniflux/miniflux/logger"
|
"github.com/miniflux/miniflux/logger"
|
||||||
|
|
||||||
"github.com/tomasen/realip"
|
"github.com/tomasen/realip"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/miniflux/miniflux/http/cookie"
|
"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/http/route"
|
||||||
"github.com/miniflux/miniflux/logger"
|
"github.com/miniflux/miniflux/logger"
|
||||||
"github.com/miniflux/miniflux/model"
|
"github.com/miniflux/miniflux/model"
|
||||||
|
@ -19,17 +21,18 @@ import (
|
||||||
// UserSession handles the user session middleware.
|
// UserSession handles the user session middleware.
|
||||||
func (m *Middleware) UserSession(next http.Handler) http.Handler {
|
func (m *Middleware) UserSession(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := m.getSessionFromCookie(r)
|
session := m.getUserSessionFromCookie(r)
|
||||||
|
|
||||||
if session == nil {
|
if session == nil {
|
||||||
logger.Debug("[Middleware:UserSession] Session not found")
|
logger.Debug("[Middleware:UserSession] Session not found")
|
||||||
if m.isPublicRoute(r) {
|
if m.isPublicRoute(r) {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
} else {
|
} else {
|
||||||
http.Redirect(w, r, route.Path(m.router, "login"), http.StatusFound)
|
response.Redirect(w, r, route.Path(m.router, "login"))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Debug("[Middleware:UserSession] %s", session)
|
logger.Debug("[Middleware:UserSession] %s", session)
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
ctx = context.WithValue(ctx, UserIDContextKey, session.UserID)
|
ctx = context.WithValue(ctx, UserIDContextKey, session.UserID)
|
||||||
ctx = context.WithValue(ctx, IsAuthenticatedContextKey, true)
|
ctx = context.WithValue(ctx, IsAuthenticatedContextKey, true)
|
||||||
|
@ -58,13 +61,13 @@ func (m *Middleware) isPublicRoute(r *http.Request) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Middleware) getSessionFromCookie(r *http.Request) *model.UserSession {
|
func (m *Middleware) getUserSessionFromCookie(r *http.Request) *model.UserSession {
|
||||||
sessionCookie, err := r.Cookie(cookie.CookieUserSessionID)
|
cookieValue := request.Cookie(r, cookie.CookieUserSessionID)
|
||||||
if err == http.ErrNoCookie {
|
if cookieValue == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := m.store.UserSessionByToken(sessionCookie.Value)
|
session, err := m.store.UserSessionByToken(cookieValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("[Middleware:UserSession] %v", err)
|
logger.Error("[Middleware:UserSession] %v", err)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -112,7 +112,8 @@ func (s *Storage) CategoriesWithFeedCount(userID int64) (model.Categories, error
|
||||||
query := `SELECT
|
query := `SELECT
|
||||||
c.id, c.user_id, c.title,
|
c.id, c.user_id, c.title,
|
||||||
(SELECT count(*) FROM feeds WHERE feeds.category_id=c.id) AS count
|
(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)
|
rows, err := s.db.Query(query, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -16,6 +16,20 @@ import (
|
||||||
"github.com/lib/pq"
|
"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
|
// NewEntryQueryBuilder returns a new EntryQueryBuilder
|
||||||
func (s *Storage) NewEntryQueryBuilder(userID int64) *EntryQueryBuilder {
|
func (s *Storage) NewEntryQueryBuilder(userID int64) *EntryQueryBuilder {
|
||||||
return NewEntryQueryBuilder(s, userID)
|
return NewEntryQueryBuilder(s, userID)
|
||||||
|
|
|
@ -47,24 +47,20 @@ func (s *Storage) UserSessions(userID int64) (model.UserSessions, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUserSession creates a new sessions.
|
// CreateUserSession creates a new sessions.
|
||||||
func (s *Storage) CreateUserSession(username, userAgent, ip string) (sessionID string, err error) {
|
func (s *Storage) CreateUserSession(username, userAgent, ip string) (sessionID string, userID int64, err error) {
|
||||||
var userID int64
|
|
||||||
|
|
||||||
err = s.db.QueryRow("SELECT id FROM users WHERE username = LOWER($1)", username).Scan(&userID)
|
err = s.db.QueryRow("SELECT id FROM users WHERE username = LOWER($1)", username).Scan(&userID)
|
||||||
if err != nil {
|
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)
|
token := crypto.GenerateRandomString(64)
|
||||||
query := "INSERT INTO user_sessions (token, user_id, user_agent, ip) VALUES ($1, $2, $3, $4)"
|
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)
|
_, err = s.db.Exec(query, token, userID, userAgent, ip)
|
||||||
if err != nil {
|
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, userID, nil
|
||||||
|
|
||||||
return token, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserSessionByToken finds a session by the token.
|
// UserSessionByToken finds a session by the token.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
// Code generated by go generate; DO NOT EDIT.
|
||||||
// 2018-04-07 13:51:33.926223471 -0700 PDT m=+0.022666283
|
// 2018-04-29 16:22:00.539326448 -0700 PDT m=+0.023616542
|
||||||
|
|
||||||
package template
|
package template
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ package template
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/miniflux/miniflux/config"
|
"github.com/miniflux/miniflux/config"
|
||||||
|
@ -38,7 +37,7 @@ func (e *Engine) parseAll() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render process a template and write the ouput.
|
// Render process a template and write the ouput.
|
||||||
func (e *Engine) Render(w io.Writer, name, language string, data interface{}) {
|
func (e *Engine) Render(name, language string, data interface{}) []byte {
|
||||||
tpl, ok := e.templates[name]
|
tpl, ok := e.templates[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Fatal("[Template] The template %s does not exists", name)
|
logger.Fatal("[Template] The template %s does not exists", name)
|
||||||
|
@ -74,7 +73,7 @@ func (e *Engine) Render(w io.Writer, name, language string, data interface{}) {
|
||||||
logger.Fatal("[Template] Unable to render template: %v", err)
|
logger.Fatal("[Template] Unable to render template: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteTo(w)
|
return b.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEngine returns a new template engine.
|
// NewEngine returns a new template engine.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
// Code generated by go generate; DO NOT EDIT.
|
||||||
// 2018-04-07 13:51:33.918407222 -0700 PDT m=+0.014850034
|
// 2018-04-29 16:22:00.531039167 -0700 PDT m=+0.015329261
|
||||||
|
|
||||||
package template
|
package template
|
||||||
|
|
||||||
|
|
31
ui/about.go
31
ui/about.go
|
@ -5,21 +5,32 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
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"
|
"github.com/miniflux/miniflux/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AboutPage shows the about page.
|
// About shows the about page.
|
||||||
func (c *Controller) AboutPage(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) About(w http.ResponseWriter, r *http.Request) {
|
||||||
args, err := c.getCommonTemplateArgs(ctx)
|
ctx := context.New(r)
|
||||||
|
|
||||||
|
user, err := c.store.UserByID(ctx.UserID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.HTML().ServerError(err)
|
html.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.HTML().Render("about", ctx.UserLanguage(), args.Merge(tplParams{
|
sess := session.New(c.store, ctx)
|
||||||
"version": version.Version,
|
view := view.New(c.tpl, ctx, sess)
|
||||||
"build_date": version.BuildDate,
|
view.Set("version", version.Version)
|
||||||
"menu": "settings",
|
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"))
|
||||||
}
|
}
|
||||||
|
|
61
ui/bookmark_entries.go
Normal file
61
ui/bookmark_entries.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
// 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))
|
||||||
|
|
||||||
|
html.OK(w, view.Render("starred"))
|
||||||
|
}
|
257
ui/category.go
257
ui/category.go
|
@ -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", ctx.UserLanguage(), 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", ctx.UserLanguage(), 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", ctx.UserLanguage(), 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", ctx.UserLanguage(), 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", ctx.UserLanguage(), 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", ctx.UserLanguage(), 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", ctx.UserLanguage(), 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", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"errorMessage": err.Error(),
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.store.AnotherCategoryExists(user.ID, category.ID, categoryForm.Title) {
|
|
||||||
response.HTML().Render("edit_category", ctx.UserLanguage(), 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", ctx.UserLanguage(), 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
|
|
||||||
}
|
|
33
ui/category_create.go
Normal file
33
ui/category_create.go
Normal file
|
@ -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"))
|
||||||
|
}
|
58
ui/category_edit.go
Normal file
58
ui/category_edit.go
Normal file
|
@ -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"))
|
||||||
|
}
|
78
ui/category_entries.go
Normal file
78
ui/category_entries.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
// 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))
|
||||||
|
|
||||||
|
html.OK(w, view.Render("category_entries"))
|
||||||
|
}
|
41
ui/category_list.go
Normal file
41
ui/category_list.go
Normal file
|
@ -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"))
|
||||||
|
}
|
50
ui/category_remove.go
Normal file
50
ui/category_remove.go
Normal file
|
@ -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"))
|
||||||
|
}
|
71
ui/category_save.go
Normal file
71
ui/category_save.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// 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"
|
||||||
|
"github.com/miniflux/miniflux/http/response/html"
|
||||||
|
"github.com/miniflux/miniflux/http/route"
|
||||||
|
"github.com/miniflux/miniflux/logger"
|
||||||
|
"github.com/miniflux/miniflux/model"
|
||||||
|
"github.com/miniflux/miniflux/ui/form"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
"github.com/miniflux/miniflux/ui/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SaveCategory validate and save the new category into the database.
|
||||||
|
func (c *Controller) SaveCategory(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
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryForm := form.NewCategoryForm(r)
|
||||||
|
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
view := view.New(c.tpl, ctx, sess)
|
||||||
|
view.Set("form", categoryForm)
|
||||||
|
view.Set("menu", "categories")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
if err := categoryForm.Validate(); err != nil {
|
||||||
|
view.Set("errorMessage", err.Error())
|
||||||
|
html.OK(w, view.Render("create_category"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicateCategory, err := c.store.CategoryByTitle(user.ID, categoryForm.Title)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if duplicateCategory != nil {
|
||||||
|
view.Set("errorMessage", "This category already exists.")
|
||||||
|
html.OK(w, view.Render("create_category"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
category := model.Category{
|
||||||
|
Title: categoryForm.Title,
|
||||||
|
UserID: user.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.store.CreateCategory(&category); err != nil {
|
||||||
|
logger.Error("[Controller:CreateCategory] %v", err)
|
||||||
|
view.Set("errorMessage", "Unable to create this category.")
|
||||||
|
html.OK(w, view.Render("create_category"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "categories"))
|
||||||
|
}
|
79
ui/category_update.go
Normal file
79
ui/category_update.go
Normal file
|
@ -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"
|
||||||
|
"github.com/miniflux/miniflux/http/response/html"
|
||||||
|
"github.com/miniflux/miniflux/http/route"
|
||||||
|
"github.com/miniflux/miniflux/logger"
|
||||||
|
"github.com/miniflux/miniflux/ui/form"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
"github.com/miniflux/miniflux/ui/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateCategory validates and updates a category.
|
||||||
|
func (c *Controller) UpdateCategory(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
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryForm := form.NewCategoryForm(r)
|
||||||
|
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
view := view.New(c.tpl, ctx, sess)
|
||||||
|
view.Set("form", categoryForm)
|
||||||
|
view.Set("category", category)
|
||||||
|
view.Set("menu", "categories")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
if err := categoryForm.Validate(); err != nil {
|
||||||
|
view.Set("errorMessage", err.Error())
|
||||||
|
html.OK(w, view.Render("edit_category"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.store.AnotherCategoryExists(user.ID, category.ID, categoryForm.Title) {
|
||||||
|
view.Set("errorMessage", "This category already exists.")
|
||||||
|
html.OK(w, view.Render("edit_category"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.store.UpdateCategory(categoryForm.Merge(category))
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[Controller:UpdateCategory] %v", err)
|
||||||
|
view.Set("errorMessage", "Unable to update this category.")
|
||||||
|
html.OK(w, view.Render("edit_category"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "categories"))
|
||||||
|
}
|
|
@ -5,59 +5,35 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/gorilla/mux"
|
||||||
"github.com/miniflux/miniflux/config"
|
"github.com/miniflux/miniflux/config"
|
||||||
"github.com/miniflux/miniflux/http/handler"
|
"github.com/miniflux/miniflux/locale"
|
||||||
"github.com/miniflux/miniflux/model"
|
|
||||||
"github.com/miniflux/miniflux/reader/feed"
|
"github.com/miniflux/miniflux/reader/feed"
|
||||||
"github.com/miniflux/miniflux/scheduler"
|
"github.com/miniflux/miniflux/scheduler"
|
||||||
"github.com/miniflux/miniflux/storage"
|
"github.com/miniflux/miniflux/storage"
|
||||||
|
"github.com/miniflux/miniflux/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tplParams map[string]interface{}
|
|
||||||
|
|
||||||
func (t tplParams) Merge(d tplParams) tplParams {
|
|
||||||
for k, v := range d {
|
|
||||||
t[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
// Controller contains all HTTP handlers for the user interface.
|
// Controller contains all HTTP handlers for the user interface.
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
store *storage.Storage
|
store *storage.Storage
|
||||||
pool *scheduler.WorkerPool
|
pool *scheduler.WorkerPool
|
||||||
feedHandler *feed.Handler
|
feedHandler *feed.Handler
|
||||||
}
|
tpl *template.Engine
|
||||||
|
router *mux.Router
|
||||||
func (c *Controller) getCommonTemplateArgs(ctx *handler.Context) (tplParams, error) {
|
translator *locale.Translator
|
||||||
user := ctx.LoggedUser()
|
|
||||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
|
||||||
builder.WithStatus(model.EntryStatusUnread)
|
|
||||||
|
|
||||||
countUnread, err := builder.CountEntries()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
params := tplParams{
|
|
||||||
"menu": "",
|
|
||||||
"user": user,
|
|
||||||
"countUnread": countUnread,
|
|
||||||
"csrf": ctx.CSRF(),
|
|
||||||
"flashMessage": ctx.FlashMessage(),
|
|
||||||
"flashErrorMessage": ctx.FlashErrorMessage(),
|
|
||||||
}
|
|
||||||
return params, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewController returns a new Controller.
|
// NewController returns a new Controller.
|
||||||
func NewController(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler) *Controller {
|
func NewController(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler, tpl *template.Engine, translator *locale.Translator, router *mux.Router) *Controller {
|
||||||
return &Controller{
|
return &Controller{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
store: store,
|
store: store,
|
||||||
pool: pool,
|
pool: pool,
|
||||||
feedHandler: feedHandler,
|
feedHandler: feedHandler,
|
||||||
|
tpl: tpl,
|
||||||
|
translator: translator,
|
||||||
|
router: router,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
494
ui/entry.go
494
ui/entry.go
|
@ -1,494 +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/integration"
|
|
||||||
"github.com/miniflux/miniflux/logger"
|
|
||||||
"github.com/miniflux/miniflux/model"
|
|
||||||
"github.com/miniflux/miniflux/reader/sanitizer"
|
|
||||||
"github.com/miniflux/miniflux/reader/scraper"
|
|
||||||
"github.com/miniflux/miniflux/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FetchContent downloads the original HTML page and returns relevant contents.
|
|
||||||
func (c *Controller) FetchContent(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
entryID, err := request.IntegerParam("entryID")
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().BadRequest(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
|
||||||
builder.WithEntryID(entryID)
|
|
||||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
|
||||||
|
|
||||||
entry, err := builder.GetEntry()
|
|
||||||
if err != nil {
|
|
||||||
response.JSON().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry == nil {
|
|
||||||
response.JSON().NotFound(errors.New("Entry not found"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := scraper.Fetch(entry.URL, entry.Feed.ScraperRules)
|
|
||||||
if err != nil {
|
|
||||||
response.JSON().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.Content = sanitizer.Sanitize(entry.URL, content)
|
|
||||||
c.store.UpdateEntryContent(entry)
|
|
||||||
|
|
||||||
response.JSON().Created(map[string]string{"content": entry.Content})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveEntry send the link to external services.
|
|
||||||
func (c *Controller) SaveEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
entryID, err := request.IntegerParam("entryID")
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().BadRequest(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
|
||||||
builder.WithEntryID(entryID)
|
|
||||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
|
||||||
|
|
||||||
entry, err := builder.GetEntry()
|
|
||||||
if err != nil {
|
|
||||||
response.JSON().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry == nil {
|
|
||||||
response.JSON().NotFound(errors.New("Entry not found"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
settings, err := c.store.Integration(user.ID)
|
|
||||||
if err != nil {
|
|
||||||
response.JSON().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
integration.SendEntry(entry, settings)
|
|
||||||
}()
|
|
||||||
|
|
||||||
response.JSON().Created(map[string]string{"message": "saved"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShowFeedEntry shows a single feed entry in "feed" mode.
|
|
||||||
func (c *Controller) ShowFeedEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
|
|
||||||
entryID, err := request.IntegerParam("entryID")
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().BadRequest(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
feedID, err := request.IntegerParam("feedID")
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().BadRequest(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
|
||||||
builder.WithFeedID(feedID)
|
|
||||||
builder.WithEntryID(entryID)
|
|
||||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
|
||||||
|
|
||||||
entry, err := builder.GetEntry()
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry == nil {
|
|
||||||
response.HTML().NotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry.Status == model.EntryStatusUnread {
|
|
||||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[Controller:ShowFeedEntry] %v", err)
|
|
||||||
response.HTML().ServerError(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args, err := c.getCommonTemplateArgs(ctx)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
builder = c.store.NewEntryQueryBuilder(user.ID)
|
|
||||||
builder.WithFeedID(feedID)
|
|
||||||
|
|
||||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nextEntryRoute := ""
|
|
||||||
if nextEntry != nil {
|
|
||||||
nextEntryRoute = ctx.Route("feedEntry", "feedID", feedID, "entryID", nextEntry.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
prevEntryRoute := ""
|
|
||||||
if prevEntry != nil {
|
|
||||||
prevEntryRoute = ctx.Route("feedEntry", "feedID", feedID, "entryID", prevEntry.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
response.HTML().Render("entry", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"entry": entry,
|
|
||||||
"prevEntry": prevEntry,
|
|
||||||
"nextEntry": nextEntry,
|
|
||||||
"nextEntryRoute": nextEntryRoute,
|
|
||||||
"prevEntryRoute": prevEntryRoute,
|
|
||||||
"menu": "feeds",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShowCategoryEntry shows a single feed entry in "category" mode.
|
|
||||||
func (c *Controller) ShowCategoryEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
|
|
||||||
categoryID, err := request.IntegerParam("categoryID")
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().BadRequest(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
entryID, err := request.IntegerParam("entryID")
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().BadRequest(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
|
||||||
builder.WithCategoryID(categoryID)
|
|
||||||
builder.WithEntryID(entryID)
|
|
||||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
|
||||||
|
|
||||||
entry, err := builder.GetEntry()
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry == nil {
|
|
||||||
response.HTML().NotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry.Status == model.EntryStatusUnread {
|
|
||||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[Controller:ShowCategoryEntry] %v", err)
|
|
||||||
response.HTML().ServerError(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args, err := c.getCommonTemplateArgs(ctx)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
builder = c.store.NewEntryQueryBuilder(user.ID)
|
|
||||||
builder.WithCategoryID(categoryID)
|
|
||||||
|
|
||||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nextEntryRoute := ""
|
|
||||||
if nextEntry != nil {
|
|
||||||
nextEntryRoute = ctx.Route("categoryEntry", "categoryID", categoryID, "entryID", nextEntry.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
prevEntryRoute := ""
|
|
||||||
if prevEntry != nil {
|
|
||||||
prevEntryRoute = ctx.Route("categoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
response.HTML().Render("entry", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"entry": entry,
|
|
||||||
"prevEntry": prevEntry,
|
|
||||||
"nextEntry": nextEntry,
|
|
||||||
"nextEntryRoute": nextEntryRoute,
|
|
||||||
"prevEntryRoute": prevEntryRoute,
|
|
||||||
"menu": "categories",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShowUnreadEntry shows a single feed entry in "unread" mode.
|
|
||||||
func (c *Controller) ShowUnreadEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
|
|
||||||
entryID, err := request.IntegerParam("entryID")
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().BadRequest(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
|
||||||
builder.WithEntryID(entryID)
|
|
||||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
|
||||||
|
|
||||||
entry, err := builder.GetEntry()
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry == nil {
|
|
||||||
response.HTML().NotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
builder = c.store.NewEntryQueryBuilder(user.ID)
|
|
||||||
builder.WithStatus(model.EntryStatusUnread)
|
|
||||||
|
|
||||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nextEntryRoute := ""
|
|
||||||
if nextEntry != nil {
|
|
||||||
nextEntryRoute = ctx.Route("unreadEntry", "entryID", nextEntry.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
prevEntryRoute := ""
|
|
||||||
if prevEntry != nil {
|
|
||||||
prevEntryRoute = ctx.Route("unreadEntry", "entryID", prevEntry.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We change the status here, otherwise we cannot get the pagination for unread items.
|
|
||||||
if entry.Status == model.EntryStatusUnread {
|
|
||||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[Controller:ShowUnreadEntry] %v", err)
|
|
||||||
response.HTML().ServerError(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The unread counter have to be fetched after changing the entry status
|
|
||||||
args, err := c.getCommonTemplateArgs(ctx)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.HTML().Render("entry", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"entry": entry,
|
|
||||||
"prevEntry": prevEntry,
|
|
||||||
"nextEntry": nextEntry,
|
|
||||||
"nextEntryRoute": nextEntryRoute,
|
|
||||||
"prevEntryRoute": prevEntryRoute,
|
|
||||||
"menu": "unread",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShowReadEntry shows a single feed entry in "history" mode.
|
|
||||||
func (c *Controller) ShowReadEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
|
|
||||||
entryID, err := request.IntegerParam("entryID")
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().BadRequest(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
|
||||||
builder.WithEntryID(entryID)
|
|
||||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
|
||||||
|
|
||||||
entry, err := builder.GetEntry()
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry == nil {
|
|
||||||
response.HTML().NotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
args, err := c.getCommonTemplateArgs(ctx)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
builder = c.store.NewEntryQueryBuilder(user.ID)
|
|
||||||
builder.WithStatus(model.EntryStatusRead)
|
|
||||||
|
|
||||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nextEntryRoute := ""
|
|
||||||
if nextEntry != nil {
|
|
||||||
nextEntryRoute = ctx.Route("readEntry", "entryID", nextEntry.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
prevEntryRoute := ""
|
|
||||||
if prevEntry != nil {
|
|
||||||
prevEntryRoute = ctx.Route("readEntry", "entryID", prevEntry.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
response.HTML().Render("entry", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"entry": entry,
|
|
||||||
"prevEntry": prevEntry,
|
|
||||||
"nextEntry": nextEntry,
|
|
||||||
"nextEntryRoute": nextEntryRoute,
|
|
||||||
"prevEntryRoute": prevEntryRoute,
|
|
||||||
"menu": "history",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShowStarredEntry shows a single feed entry in "starred" mode.
|
|
||||||
func (c *Controller) ShowStarredEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
|
|
||||||
entryID, err := request.IntegerParam("entryID")
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().BadRequest(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
|
||||||
builder.WithEntryID(entryID)
|
|
||||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
|
||||||
|
|
||||||
entry, err := builder.GetEntry()
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry == nil {
|
|
||||||
response.HTML().NotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry.Status == model.EntryStatusUnread {
|
|
||||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[Controller:ShowReadEntry] %v", err)
|
|
||||||
response.HTML().ServerError(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args, err := c.getCommonTemplateArgs(ctx)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
builder = c.store.NewEntryQueryBuilder(user.ID)
|
|
||||||
builder.WithStarred()
|
|
||||||
|
|
||||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nextEntryRoute := ""
|
|
||||||
if nextEntry != nil {
|
|
||||||
nextEntryRoute = ctx.Route("starredEntry", "entryID", nextEntry.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
prevEntryRoute := ""
|
|
||||||
if prevEntry != nil {
|
|
||||||
prevEntryRoute = ctx.Route("starredEntry", "entryID", prevEntry.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
response.HTML().Render("entry", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"entry": entry,
|
|
||||||
"prevEntry": prevEntry,
|
|
||||||
"nextEntry": nextEntry,
|
|
||||||
"nextEntryRoute": nextEntryRoute,
|
|
||||||
"prevEntryRoute": prevEntryRoute,
|
|
||||||
"menu": "starred",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateEntriesStatus handles Ajax request to update the status for a list of entries.
|
|
||||||
func (c *Controller) UpdateEntriesStatus(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
|
|
||||||
entryIDs, status, err := decodeEntryStatusPayload(request.Body())
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[Controller:UpdateEntryStatus] %v", err)
|
|
||||||
response.JSON().BadRequest(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(entryIDs) == 0 {
|
|
||||||
response.JSON().BadRequest(errors.New("The list of entryID is empty"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.store.SetEntriesStatus(user.ID, entryIDs, status)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[Controller:UpdateEntryStatus] %v", err)
|
|
||||||
response.JSON().ServerError(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.JSON().Standard("OK")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) getEntryPrevNext(user *model.User, builder *storage.EntryQueryBuilder, entryID int64) (prev *model.Entry, next *model.Entry, err error) {
|
|
||||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
|
||||||
builder.WithOrder(model.DefaultSortingOrder)
|
|
||||||
builder.WithDirection(user.EntryDirection)
|
|
||||||
|
|
||||||
entries, err := builder.GetEntries()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
n := len(entries)
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
if entries[i].ID == entryID {
|
|
||||||
if i-1 >= 0 {
|
|
||||||
prev = entries[i-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
if i+1 < n {
|
|
||||||
next = entries[i+1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return prev, next, nil
|
|
||||||
}
|
|
98
ui/entry_category.go
Normal file
98
ui/entry_category.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
// 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/logger"
|
||||||
|
"github.com/miniflux/miniflux/model"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
"github.com/miniflux/miniflux/ui/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShowCategoryEntry shows a single feed entry in "category" mode.
|
||||||
|
func (c *Controller) ShowCategoryEntry(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
|
||||||
|
}
|
||||||
|
|
||||||
|
entryID, err := request.IntParam(r, "entryID")
|
||||||
|
if err != nil {
|
||||||
|
html.BadRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||||
|
builder.WithCategoryID(categoryID)
|
||||||
|
builder.WithEntryID(entryID)
|
||||||
|
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||||
|
|
||||||
|
entry, err := builder.GetEntry()
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry == nil {
|
||||||
|
html.NotFound(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.Status == model.EntryStatusUnread {
|
||||||
|
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[Controller:ShowCategoryEntry] %v", err)
|
||||||
|
html.ServerError(w, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder = c.store.NewEntryQueryBuilder(user.ID)
|
||||||
|
builder.WithCategoryID(categoryID)
|
||||||
|
|
||||||
|
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nextEntryRoute := ""
|
||||||
|
if nextEntry != nil {
|
||||||
|
nextEntryRoute = route.Path(c.router, "categoryEntry", "categoryID", categoryID, "entryID", nextEntry.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
prevEntryRoute := ""
|
||||||
|
if prevEntry != nil {
|
||||||
|
prevEntryRoute = route.Path(c.router, "categoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
view := view.New(c.tpl, ctx, sess)
|
||||||
|
view.Set("entry", entry)
|
||||||
|
view.Set("prevEntry", prevEntry)
|
||||||
|
view.Set("nextEntry", nextEntry)
|
||||||
|
view.Set("nextEntryRoute", nextEntryRoute)
|
||||||
|
view.Set("prevEntryRoute", prevEntryRoute)
|
||||||
|
view.Set("menu", "categories")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
html.OK(w, view.Render("entry"))
|
||||||
|
}
|
98
ui/entry_feed.go
Normal file
98
ui/entry_feed.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
// 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/logger"
|
||||||
|
"github.com/miniflux/miniflux/model"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
"github.com/miniflux/miniflux/ui/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShowFeedEntry shows a single feed entry in "feed" mode.
|
||||||
|
func (c *Controller) ShowFeedEntry(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
|
||||||
|
}
|
||||||
|
|
||||||
|
entryID, err := request.IntParam(r, "entryID")
|
||||||
|
if err != nil {
|
||||||
|
html.BadRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
feedID, err := request.IntParam(r, "feedID")
|
||||||
|
if err != nil {
|
||||||
|
html.BadRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||||
|
builder.WithFeedID(feedID)
|
||||||
|
builder.WithEntryID(entryID)
|
||||||
|
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||||
|
|
||||||
|
entry, err := builder.GetEntry()
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry == nil {
|
||||||
|
html.NotFound(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.Status == model.EntryStatusUnread {
|
||||||
|
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[Controller:ShowFeedEntry] %v", err)
|
||||||
|
html.ServerError(w, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder = c.store.NewEntryQueryBuilder(user.ID)
|
||||||
|
builder.WithFeedID(feedID)
|
||||||
|
|
||||||
|
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nextEntryRoute := ""
|
||||||
|
if nextEntry != nil {
|
||||||
|
nextEntryRoute = route.Path(c.router, "feedEntry", "feedID", feedID, "entryID", nextEntry.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
prevEntryRoute := ""
|
||||||
|
if prevEntry != nil {
|
||||||
|
prevEntryRoute = route.Path(c.router, "feedEntry", "feedID", feedID, "entryID", prevEntry.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
view := view.New(c.tpl, ctx, sess)
|
||||||
|
view.Set("entry", entry)
|
||||||
|
view.Set("prevEntry", prevEntry)
|
||||||
|
view.Set("nextEntry", nextEntry)
|
||||||
|
view.Set("nextEntryRoute", nextEntryRoute)
|
||||||
|
view.Set("prevEntryRoute", prevEntryRoute)
|
||||||
|
view.Set("menu", "feeds")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
html.OK(w, view.Render("entry"))
|
||||||
|
}
|
36
ui/entry_prev_next.go
Normal file
36
ui/entry_prev_next.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// 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 (
|
||||||
|
"github.com/miniflux/miniflux/model"
|
||||||
|
"github.com/miniflux/miniflux/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Controller) getEntryPrevNext(user *model.User, builder *storage.EntryQueryBuilder, entryID int64) (prev *model.Entry, next *model.Entry, err error) {
|
||||||
|
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||||
|
builder.WithOrder(model.DefaultSortingOrder)
|
||||||
|
builder.WithDirection(user.EntryDirection)
|
||||||
|
|
||||||
|
entries, err := builder.GetEntries()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
n := len(entries)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if entries[i].ID == entryID {
|
||||||
|
if i-1 >= 0 {
|
||||||
|
prev = entries[i-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if i+1 < n {
|
||||||
|
next = entries[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev, next, nil
|
||||||
|
}
|
81
ui/entry_read.go
Normal file
81
ui/entry_read.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShowReadEntry shows a single feed entry in "history" mode.
|
||||||
|
func (c *Controller) ShowReadEntry(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
|
||||||
|
}
|
||||||
|
|
||||||
|
entryID, err := request.IntParam(r, "entryID")
|
||||||
|
if err != nil {
|
||||||
|
html.BadRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||||
|
builder.WithEntryID(entryID)
|
||||||
|
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||||
|
|
||||||
|
entry, err := builder.GetEntry()
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry == nil {
|
||||||
|
html.NotFound(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
builder = c.store.NewEntryQueryBuilder(user.ID)
|
||||||
|
builder.WithStatus(model.EntryStatusRead)
|
||||||
|
|
||||||
|
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nextEntryRoute := ""
|
||||||
|
if nextEntry != nil {
|
||||||
|
nextEntryRoute = route.Path(c.router, "readEntry", "entryID", nextEntry.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
prevEntryRoute := ""
|
||||||
|
if prevEntry != nil {
|
||||||
|
prevEntryRoute = route.Path(c.router, "readEntry", "entryID", prevEntry.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
view := view.New(c.tpl, ctx, sess)
|
||||||
|
view.Set("entry", entry)
|
||||||
|
view.Set("prevEntry", prevEntry)
|
||||||
|
view.Set("nextEntry", nextEntry)
|
||||||
|
view.Set("nextEntryRoute", nextEntryRoute)
|
||||||
|
view.Set("prevEntryRoute", prevEntryRoute)
|
||||||
|
view.Set("menu", "history")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
html.OK(w, view.Render("entry"))
|
||||||
|
}
|
54
ui/entry_save.go
Normal file
54
ui/entry_save.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// 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 (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"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/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SaveEntry send the link to external services.
|
||||||
|
func (c *Controller) SaveEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
|
entryID, err := request.IntParam(r, "entryID")
|
||||||
|
if err != nil {
|
||||||
|
json.BadRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.New(r)
|
||||||
|
|
||||||
|
builder := c.store.NewEntryQueryBuilder(ctx.UserID())
|
||||||
|
builder.WithEntryID(entryID)
|
||||||
|
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||||
|
|
||||||
|
entry, err := builder.GetEntry()
|
||||||
|
if err != nil {
|
||||||
|
json.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry == nil {
|
||||||
|
json.NotFound(w, errors.New("Entry not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, err := c.store.Integration(ctx.UserID())
|
||||||
|
if err != nil {
|
||||||
|
json.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
integration.SendEntry(entry, settings)
|
||||||
|
}()
|
||||||
|
|
||||||
|
json.Created(w, map[string]string{"message": "saved"})
|
||||||
|
}
|
53
ui/entry_scraper.go
Normal file
53
ui/entry_scraper.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// 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 (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/miniflux/miniflux/http/context"
|
||||||
|
"github.com/miniflux/miniflux/http/request"
|
||||||
|
"github.com/miniflux/miniflux/http/response/json"
|
||||||
|
"github.com/miniflux/miniflux/model"
|
||||||
|
"github.com/miniflux/miniflux/reader/sanitizer"
|
||||||
|
"github.com/miniflux/miniflux/reader/scraper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FetchContent downloads the original HTML page and returns relevant contents.
|
||||||
|
func (c *Controller) FetchContent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
entryID, err := request.IntParam(r, "entryID")
|
||||||
|
if err != nil {
|
||||||
|
json.BadRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.New(r)
|
||||||
|
builder := c.store.NewEntryQueryBuilder(ctx.UserID())
|
||||||
|
builder.WithEntryID(entryID)
|
||||||
|
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||||
|
|
||||||
|
entry, err := builder.GetEntry()
|
||||||
|
if err != nil {
|
||||||
|
json.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry == nil {
|
||||||
|
json.NotFound(w, errors.New("Entry not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := scraper.Fetch(entry.URL, entry.Feed.ScraperRules)
|
||||||
|
if err != nil {
|
||||||
|
json.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.Content = sanitizer.Sanitize(entry.URL, content)
|
||||||
|
c.store.UpdateEntryContent(entry)
|
||||||
|
|
||||||
|
json.Created(w, map[string]string{"content": entry.Content})
|
||||||
|
}
|
91
ui/entry_starred.go
Normal file
91
ui/entry_starred.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
// 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/logger"
|
||||||
|
"github.com/miniflux/miniflux/model"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
"github.com/miniflux/miniflux/ui/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShowStarredEntry shows a single feed entry in "starred" mode.
|
||||||
|
func (c *Controller) ShowStarredEntry(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
|
||||||
|
}
|
||||||
|
|
||||||
|
entryID, err := request.IntParam(r, "entryID")
|
||||||
|
if err != nil {
|
||||||
|
html.BadRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||||
|
builder.WithEntryID(entryID)
|
||||||
|
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||||
|
|
||||||
|
entry, err := builder.GetEntry()
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry == nil {
|
||||||
|
html.NotFound(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.Status == model.EntryStatusUnread {
|
||||||
|
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[Controller:ShowReadEntry] %v", err)
|
||||||
|
html.ServerError(w, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder = c.store.NewEntryQueryBuilder(user.ID)
|
||||||
|
builder.WithStarred()
|
||||||
|
|
||||||
|
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nextEntryRoute := ""
|
||||||
|
if nextEntry != nil {
|
||||||
|
nextEntryRoute = route.Path(c.router, "starredEntry", "entryID", nextEntry.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
prevEntryRoute := ""
|
||||||
|
if prevEntry != nil {
|
||||||
|
prevEntryRoute = route.Path(c.router, "starredEntry", "entryID", prevEntry.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
view := view.New(c.tpl, ctx, sess)
|
||||||
|
view.Set("entry", entry)
|
||||||
|
view.Set("prevEntry", prevEntry)
|
||||||
|
view.Set("nextEntry", nextEntry)
|
||||||
|
view.Set("nextEntryRoute", nextEntryRoute)
|
||||||
|
view.Set("prevEntryRoute", prevEntryRoute)
|
||||||
|
view.Set("menu", "starred")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
html.OK(w, view.Render("entry"))
|
||||||
|
}
|
32
ui/entry_toggle_bookmark.go
Normal file
32
ui/entry_toggle_bookmark.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// 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/json"
|
||||||
|
"github.com/miniflux/miniflux/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToggleBookmark handles Ajax request to toggle bookmark value.
|
||||||
|
func (c *Controller) ToggleBookmark(w http.ResponseWriter, r *http.Request) {
|
||||||
|
entryID, err := request.IntParam(r, "entryID")
|
||||||
|
if err != nil {
|
||||||
|
json.BadRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.New(r)
|
||||||
|
if err := c.store.ToggleBookmark(ctx.UserID(), entryID); err != nil {
|
||||||
|
logger.Error("[Controller:ToggleBookmark] %v", err)
|
||||||
|
json.ServerError(w, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.OK(w, "OK")
|
||||||
|
}
|
92
ui/entry_unread.go
Normal file
92
ui/entry_unread.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
// 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/logger"
|
||||||
|
"github.com/miniflux/miniflux/model"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
"github.com/miniflux/miniflux/ui/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShowUnreadEntry shows a single feed entry in "unread" mode.
|
||||||
|
func (c *Controller) ShowUnreadEntry(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
|
||||||
|
}
|
||||||
|
|
||||||
|
entryID, err := request.IntParam(r, "entryID")
|
||||||
|
if err != nil {
|
||||||
|
html.BadRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||||
|
builder.WithEntryID(entryID)
|
||||||
|
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||||
|
|
||||||
|
entry, err := builder.GetEntry()
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry == nil {
|
||||||
|
html.NotFound(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
builder = c.store.NewEntryQueryBuilder(user.ID)
|
||||||
|
builder.WithStatus(model.EntryStatusUnread)
|
||||||
|
|
||||||
|
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nextEntryRoute := ""
|
||||||
|
if nextEntry != nil {
|
||||||
|
nextEntryRoute = route.Path(c.router, "unreadEntry", "entryID", nextEntry.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
prevEntryRoute := ""
|
||||||
|
if prevEntry != nil {
|
||||||
|
prevEntryRoute = route.Path(c.router, "unreadEntry", "entryID", prevEntry.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We change the status here, otherwise we cannot get the pagination for unread items.
|
||||||
|
if entry.Status == model.EntryStatusUnread {
|
||||||
|
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[Controller:ShowUnreadEntry] %v", err)
|
||||||
|
html.ServerError(w, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
view := view.New(c.tpl, ctx, sess)
|
||||||
|
view.Set("entry", entry)
|
||||||
|
view.Set("prevEntry", prevEntry)
|
||||||
|
view.Set("nextEntry", nextEntry)
|
||||||
|
view.Set("nextEntryRoute", nextEntryRoute)
|
||||||
|
view.Set("prevEntryRoute", prevEntryRoute)
|
||||||
|
view.Set("menu", "unread")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
html.OK(w, view.Render("entry"))
|
||||||
|
}
|
39
ui/entry_update_status.go
Normal file
39
ui/entry_update_status.go
Normal file
|
@ -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 ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/miniflux/miniflux/http/context"
|
||||||
|
"github.com/miniflux/miniflux/http/response/json"
|
||||||
|
"github.com/miniflux/miniflux/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateEntriesStatus updates the status for a list of entries.
|
||||||
|
func (c *Controller) UpdateEntriesStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
entryIDs, status, err := decodeEntryStatusPayload(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[Controller:UpdateEntryStatus] %v", err)
|
||||||
|
json.BadRequest(w, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entryIDs) == 0 {
|
||||||
|
json.BadRequest(w, errors.New("The list of entryID is empty"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.New(r)
|
||||||
|
err = c.store.SetEntriesStatus(ctx.UserID(), entryIDs, status)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[Controller:UpdateEntryStatus] %v", err)
|
||||||
|
json.ServerError(w, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.OK(w, "OK")
|
||||||
|
}
|
236
ui/feed.go
236
ui/feed.go
|
@ -1,236 +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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RefreshAllFeeds refresh all feeds in the background for the current user.
|
|
||||||
func (c *Controller) RefreshAllFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
jobs, err := c.store.NewUserBatch(user.ID, c.store.CountFeeds(user.ID))
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
c.pool.Push(jobs)
|
|
||||||
}()
|
|
||||||
|
|
||||||
response.Redirect(ctx.Route("feeds"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShowFeedsPage shows the page with all subscriptions.
|
|
||||||
func (c *Controller) ShowFeedsPage(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
|
|
||||||
}
|
|
||||||
|
|
||||||
feeds, err := c.store.Feeds(user.ID)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.HTML().Render("feeds", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"feeds": feeds,
|
|
||||||
"total": len(feeds),
|
|
||||||
"menu": "feeds",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShowFeedEntries shows all entries for the given feed.
|
|
||||||
func (c *Controller) ShowFeedEntries(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
|
|
||||||
}
|
|
||||||
|
|
||||||
feed, err := c.getFeedFromURL(request, response, user)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
|
||||||
builder.WithFeedID(feed.ID)
|
|
||||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
|
||||||
builder.WithOrder(model.DefaultSortingOrder)
|
|
||||||
builder.WithDirection(user.EntryDirection)
|
|
||||||
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("feed_entries", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"feed": feed,
|
|
||||||
"entries": entries,
|
|
||||||
"total": count,
|
|
||||||
"pagination": c.getPagination(ctx.Route("feedEntries", "feedID", feed.ID), count, offset),
|
|
||||||
"menu": "feeds",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// EditFeed shows the form to modify a subscription.
|
|
||||||
func (c *Controller) EditFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
|
|
||||||
feed, err := c.getFeedFromURL(request, response, user)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
args, err := c.getFeedFormTemplateArgs(ctx, user, feed, nil)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.HTML().Render("edit_feed", ctx.UserLanguage(), args)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateFeed update a subscription and redirect to the feed entries page.
|
|
||||||
func (c *Controller) UpdateFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
|
|
||||||
feed, err := c.getFeedFromURL(request, response, user)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
feedForm := form.NewFeedForm(request.Request())
|
|
||||||
args, err := c.getFeedFormTemplateArgs(ctx, user, feed, feedForm)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := feedForm.ValidateModification(); err != nil {
|
|
||||||
response.HTML().Render("edit_feed", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"errorMessage": err.Error(),
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.store.UpdateFeed(feedForm.Merge(feed))
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[Controller:EditFeed] %v", err)
|
|
||||||
response.HTML().Render("edit_feed", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"errorMessage": "Unable to update this feed.",
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Redirect(ctx.Route("feedEntries", "feedID", feed.ID))
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveFeed delete a subscription from the database and redirect to the list of feeds page.
|
|
||||||
func (c *Controller) RemoveFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
feedID, err := request.IntegerParam("feedID")
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
if err := c.store.RemoveFeed(user.ID, feedID); err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Redirect(ctx.Route("feeds"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// RefreshFeed refresh a subscription and redirect to the feed entries page.
|
|
||||||
func (c *Controller) RefreshFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
feedID, err := request.IntegerParam("feedID")
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().BadRequest(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
if err := c.feedHandler.RefreshFeed(user.ID, feedID); err != nil {
|
|
||||||
logger.Error("[Controller:RefreshFeed] %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Redirect(ctx.Route("feedEntries", "feedID", feedID))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) getFeedFromURL(request *handler.Request, response *handler.Response, user *model.User) (*model.Feed, error) {
|
|
||||||
feedID, err := request.IntegerParam("feedID")
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().BadRequest(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
feed, err := c.store.FeedByID(user.ID, feedID)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if feed == nil {
|
|
||||||
response.HTML().NotFound()
|
|
||||||
return nil, errors.New("Feed not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return feed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) getFeedFormTemplateArgs(ctx *handler.Context, user *model.User, feed *model.Feed, feedForm *form.FeedForm) (tplParams, error) {
|
|
||||||
args, err := c.getCommonTemplateArgs(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
categories, err := c.store.Categories(user.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if feedForm == nil {
|
|
||||||
args["form"] = form.FeedForm{
|
|
||||||
SiteURL: feed.SiteURL,
|
|
||||||
FeedURL: feed.FeedURL,
|
|
||||||
Title: feed.Title,
|
|
||||||
ScraperRules: feed.ScraperRules,
|
|
||||||
RewriteRules: feed.RewriteRules,
|
|
||||||
Crawler: feed.Crawler,
|
|
||||||
CategoryID: feed.Category.ID,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
args["form"] = feedForm
|
|
||||||
}
|
|
||||||
|
|
||||||
args["categories"] = categories
|
|
||||||
args["feed"] = feed
|
|
||||||
args["menu"] = "feeds"
|
|
||||||
return args, nil
|
|
||||||
}
|
|
71
ui/feed_edit.go
Normal file
71
ui/feed_edit.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EditFeed shows the form to modify a subscription.
|
||||||
|
func (c *Controller) EditFeed(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
|
||||||
|
}
|
||||||
|
|
||||||
|
feedID, err := request.IntParam(r, "feedID")
|
||||||
|
if err != nil {
|
||||||
|
html.BadRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
feed, err := c.store.FeedByID(user.ID, feedID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if feed == nil {
|
||||||
|
html.NotFound(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
categories, err := c.store.Categories(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
feedForm := form.FeedForm{
|
||||||
|
SiteURL: feed.SiteURL,
|
||||||
|
FeedURL: feed.FeedURL,
|
||||||
|
Title: feed.Title,
|
||||||
|
ScraperRules: feed.ScraperRules,
|
||||||
|
RewriteRules: feed.RewriteRules,
|
||||||
|
Crawler: feed.Crawler,
|
||||||
|
CategoryID: feed.Category.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
view := view.New(c.tpl, ctx, sess)
|
||||||
|
view.Set("form", feedForm)
|
||||||
|
view.Set("categories", categories)
|
||||||
|
view.Set("feed", feed)
|
||||||
|
view.Set("menu", "feeds")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
html.OK(w, view.Render("edit_feed"))
|
||||||
|
}
|
78
ui/feed_entries.go
Normal file
78
ui/feed_entries.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShowFeedEntries shows all entries for the given feed.
|
||||||
|
func (c *Controller) ShowFeedEntries(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
|
||||||
|
}
|
||||||
|
|
||||||
|
feedID, err := request.IntParam(r, "feedID")
|
||||||
|
if err != nil {
|
||||||
|
html.BadRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
feed, err := c.store.FeedByID(user.ID, feedID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if feed == nil {
|
||||||
|
html.NotFound(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := request.QueryIntParam(r, "offset", 0)
|
||||||
|
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||||
|
builder.WithFeedID(feed.ID)
|
||||||
|
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||||
|
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("feed", feed)
|
||||||
|
view.Set("entries", entries)
|
||||||
|
view.Set("total", count)
|
||||||
|
view.Set("pagination", c.getPagination(route.Path(c.router, "feedEntries", "feedID", feed.ID), count, offset))
|
||||||
|
view.Set("menu", "feeds")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
html.OK(w, view.Render("feed_entries"))
|
||||||
|
}
|
36
ui/feed_icon.go
Normal file
36
ui/feed_icon.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// 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 (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miniflux/miniflux/http/request"
|
||||||
|
"github.com/miniflux/miniflux/http/response"
|
||||||
|
"github.com/miniflux/miniflux/http/response/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShowIcon shows the feed icon.
|
||||||
|
func (c *Controller) ShowIcon(w http.ResponseWriter, r *http.Request) {
|
||||||
|
iconID, err := request.IntParam(r, "iconID")
|
||||||
|
if err != nil {
|
||||||
|
html.BadRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
icon, err := c.store.IconByID(iconID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if icon == nil {
|
||||||
|
html.NotFound(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Cache(w, r, icon.MimeType, icon.Hash, icon.Content, 72*time.Hour)
|
||||||
|
}
|
41
ui/feed_list.go
Normal file
41
ui/feed_list.go
Normal file
|
@ -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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShowFeedsPage shows the page with all subscriptions.
|
||||||
|
func (c *Controller) ShowFeedsPage(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
|
||||||
|
}
|
||||||
|
|
||||||
|
feeds, err := c.store.Feeds(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("feeds", feeds)
|
||||||
|
view.Set("total", len(feeds))
|
||||||
|
view.Set("menu", "feeds")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
html.OK(w, view.Render("feeds"))
|
||||||
|
}
|
48
ui/feed_refresh.go
Normal file
48
ui/feed_refresh.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
// 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"
|
||||||
|
"github.com/miniflux/miniflux/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RefreshFeed refresh a subscription and redirect to the feed entries page.
|
||||||
|
func (c *Controller) RefreshFeed(w http.ResponseWriter, r *http.Request) {
|
||||||
|
feedID, err := request.IntParam(r, "feedID")
|
||||||
|
if err != nil {
|
||||||
|
html.BadRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.New(r)
|
||||||
|
if err := c.feedHandler.RefreshFeed(ctx.UserID(), feedID); err != nil {
|
||||||
|
logger.Error("[Controller:RefreshFeed] %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feedID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshAllFeeds refresh all feeds in the background for the current user.
|
||||||
|
func (c *Controller) RefreshAllFeeds(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID := context.New(r).UserID()
|
||||||
|
jobs, err := c.store.NewUserBatch(userID, c.store.CountFeeds(userID))
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
c.pool.Push(jobs)
|
||||||
|
}()
|
||||||
|
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "feeds"))
|
||||||
|
}
|
32
ui/feed_remove.go
Normal file
32
ui/feed_remove.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RemoveFeed deletes a subscription from the database and redirect to the list of feeds page.
|
||||||
|
func (c *Controller) RemoveFeed(w http.ResponseWriter, r *http.Request) {
|
||||||
|
feedID, err := request.IntParam(r, "feedID")
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.New(r)
|
||||||
|
if err := c.store.RemoveFeed(ctx.UserID(), feedID); err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "feeds"))
|
||||||
|
}
|
80
ui/feed_update.go
Normal file
80
ui/feed_update.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
// 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"
|
||||||
|
"github.com/miniflux/miniflux/logger"
|
||||||
|
"github.com/miniflux/miniflux/ui/form"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
"github.com/miniflux/miniflux/ui/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateFeed update a subscription and redirect to the feed entries page.
|
||||||
|
func (c *Controller) UpdateFeed(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
|
||||||
|
}
|
||||||
|
|
||||||
|
feedID, err := request.IntParam(r, "feedID")
|
||||||
|
if err != nil {
|
||||||
|
html.BadRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
feed, err := c.store.FeedByID(user.ID, feedID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if feed == nil {
|
||||||
|
html.NotFound(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
categories, err := c.store.Categories(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
feedForm := form.NewFeedForm(r)
|
||||||
|
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
view := view.New(c.tpl, ctx, sess)
|
||||||
|
view.Set("form", feedForm)
|
||||||
|
view.Set("categories", categories)
|
||||||
|
view.Set("feed", feed)
|
||||||
|
view.Set("menu", "feeds")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
if err := feedForm.ValidateModification(); err != nil {
|
||||||
|
view.Set("errorMessage", err.Error())
|
||||||
|
html.OK(w, view.Render("edit_feed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.store.UpdateFeed(feedForm.Merge(feed))
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[Controller:EditFeed] %v", err)
|
||||||
|
view.Set("errorMessage", "Unable to update this feed.")
|
||||||
|
html.OK(w, view.Render("edit_feed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID))
|
||||||
|
}
|
|
@ -1,61 +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 (
|
|
||||||
"github.com/miniflux/miniflux/http/handler"
|
|
||||||
"github.com/miniflux/miniflux/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ShowHistoryPage renders the page with all read entries.
|
|
||||||
func (c *Controller) ShowHistoryPage(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
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
|
||||||
builder.WithStatus(model.EntryStatusRead)
|
|
||||||
builder.WithOrder(model.DefaultSortingOrder)
|
|
||||||
builder.WithDirection(user.EntryDirection)
|
|
||||||
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("history", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"entries": entries,
|
|
||||||
"total": count,
|
|
||||||
"pagination": c.getPagination(ctx.Route("history"), count, offset),
|
|
||||||
"menu": "history",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// FlushHistory changes all "read" items to "removed".
|
|
||||||
func (c *Controller) FlushHistory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
|
|
||||||
err := c.store.FlushHistory(user.ID)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Redirect(ctx.Route("history"))
|
|
||||||
}
|
|
59
ui/history_entries.go
Normal file
59
ui/history_entries.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// 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 (
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShowHistoryPage renders the page with all read entries.
|
||||||
|
func (c *Controller) ShowHistoryPage(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.WithStatus(model.EntryStatusRead)
|
||||||
|
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("entries", entries)
|
||||||
|
view.Set("total", count)
|
||||||
|
view.Set("pagination", c.getPagination(route.Path(c.router, "history"), count, offset))
|
||||||
|
view.Set("menu", "history")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
html.OK(w, view.Render("history"))
|
||||||
|
}
|
25
ui/history_flush.go
Normal file
25
ui/history_flush.go
Normal file
|
@ -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 ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/miniflux/miniflux/http/context"
|
||||||
|
"github.com/miniflux/miniflux/http/response"
|
||||||
|
"github.com/miniflux/miniflux/http/response/html"
|
||||||
|
"github.com/miniflux/miniflux/http/route"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FlushHistory changes all "read" items to "removed".
|
||||||
|
func (c *Controller) FlushHistory(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := c.store.FlushHistory(context.New(r).UserID())
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "history"))
|
||||||
|
}
|
33
ui/icon.go
33
ui/icon.go
|
@ -1,33 +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 (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/miniflux/miniflux/http/handler"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ShowIcon shows the feed icon.
|
|
||||||
func (c *Controller) ShowIcon(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
iconID, err := request.IntegerParam("iconID")
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().BadRequest(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
icon, err := c.store.IconByID(iconID)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if icon == nil {
|
|
||||||
response.HTML().NotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Cache(icon.MimeType, icon.Hash, icon.Content, 72*time.Hour)
|
|
||||||
}
|
|
63
ui/integration_show.go
Normal file
63
ui/integration_show.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
// 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 (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/miniflux/miniflux/http/context"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShowIntegrations renders the page with all external integrations.
|
||||||
|
func (c *Controller) ShowIntegrations(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
|
||||||
|
}
|
||||||
|
|
||||||
|
integration, err := c.store.Integration(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
integrationForm := form.IntegrationForm{
|
||||||
|
PinboardEnabled: integration.PinboardEnabled,
|
||||||
|
PinboardToken: integration.PinboardToken,
|
||||||
|
PinboardTags: integration.PinboardTags,
|
||||||
|
PinboardMarkAsUnread: integration.PinboardMarkAsUnread,
|
||||||
|
InstapaperEnabled: integration.InstapaperEnabled,
|
||||||
|
InstapaperUsername: integration.InstapaperUsername,
|
||||||
|
InstapaperPassword: integration.InstapaperPassword,
|
||||||
|
FeverEnabled: integration.FeverEnabled,
|
||||||
|
FeverUsername: integration.FeverUsername,
|
||||||
|
FeverPassword: integration.FeverPassword,
|
||||||
|
WallabagEnabled: integration.WallabagEnabled,
|
||||||
|
WallabagURL: integration.WallabagURL,
|
||||||
|
WallabagClientID: integration.WallabagClientID,
|
||||||
|
WallabagClientSecret: integration.WallabagClientSecret,
|
||||||
|
WallabagUsername: integration.WallabagUsername,
|
||||||
|
WallabagPassword: integration.WallabagPassword,
|
||||||
|
NunuxKeeperEnabled: integration.NunuxKeeperEnabled,
|
||||||
|
NunuxKeeperURL: integration.NunuxKeeperURL,
|
||||||
|
NunuxKeeperAPIKey: integration.NunuxKeeperAPIKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
view := view.New(c.tpl, ctx, sess)
|
||||||
|
view.Set("form", integrationForm)
|
||||||
|
view.Set("menu", "settings")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
html.OK(w, view.Render("integrations"))
|
||||||
|
}
|
60
ui/integration_update.go
Normal file
60
ui/integration_update.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
// 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 (
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/miniflux/miniflux/http/context"
|
||||||
|
"github.com/miniflux/miniflux/http/response"
|
||||||
|
"github.com/miniflux/miniflux/http/response/html"
|
||||||
|
"github.com/miniflux/miniflux/http/route"
|
||||||
|
"github.com/miniflux/miniflux/ui/form"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateIntegration updates integration settings.
|
||||||
|
func (c *Controller) UpdateIntegration(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := context.New(r)
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
|
||||||
|
user, err := c.store.UserByID(ctx.UserID())
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
integration, err := c.store.Integration(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
integrationForm := form.NewIntegrationForm(r)
|
||||||
|
integrationForm.Merge(integration)
|
||||||
|
|
||||||
|
if integration.FeverUsername != "" && c.store.HasDuplicateFeverUsername(user.ID, integration.FeverUsername) {
|
||||||
|
sess.NewFlashErrorMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("There is already someone else with the same Fever username!"))
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "integrations"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if integration.FeverEnabled {
|
||||||
|
integration.FeverToken = fmt.Sprintf("%x", md5.Sum([]byte(integration.FeverUsername+":"+integration.FeverPassword)))
|
||||||
|
} else {
|
||||||
|
integration.FeverToken = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.store.UpdateIntegration(integration)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess.NewFlashMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("Preferences saved!"))
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "integrations"))
|
||||||
|
}
|
|
@ -1,87 +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 (
|
|
||||||
"crypto/md5"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/miniflux/miniflux/http/handler"
|
|
||||||
"github.com/miniflux/miniflux/ui/form"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ShowIntegrations renders the page with all external integrations.
|
|
||||||
func (c *Controller) ShowIntegrations(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
integration, err := c.store.Integration(user.ID)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
args, err := c.getCommonTemplateArgs(ctx)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.HTML().Render("integrations", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"menu": "settings",
|
|
||||||
"form": form.IntegrationForm{
|
|
||||||
PinboardEnabled: integration.PinboardEnabled,
|
|
||||||
PinboardToken: integration.PinboardToken,
|
|
||||||
PinboardTags: integration.PinboardTags,
|
|
||||||
PinboardMarkAsUnread: integration.PinboardMarkAsUnread,
|
|
||||||
InstapaperEnabled: integration.InstapaperEnabled,
|
|
||||||
InstapaperUsername: integration.InstapaperUsername,
|
|
||||||
InstapaperPassword: integration.InstapaperPassword,
|
|
||||||
FeverEnabled: integration.FeverEnabled,
|
|
||||||
FeverUsername: integration.FeverUsername,
|
|
||||||
FeverPassword: integration.FeverPassword,
|
|
||||||
WallabagEnabled: integration.WallabagEnabled,
|
|
||||||
WallabagURL: integration.WallabagURL,
|
|
||||||
WallabagClientID: integration.WallabagClientID,
|
|
||||||
WallabagClientSecret: integration.WallabagClientSecret,
|
|
||||||
WallabagUsername: integration.WallabagUsername,
|
|
||||||
WallabagPassword: integration.WallabagPassword,
|
|
||||||
NunuxKeeperEnabled: integration.NunuxKeeperEnabled,
|
|
||||||
NunuxKeeperURL: integration.NunuxKeeperURL,
|
|
||||||
NunuxKeeperAPIKey: integration.NunuxKeeperAPIKey,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateIntegration updates integration settings.
|
|
||||||
func (c *Controller) UpdateIntegration(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
integration, err := c.store.Integration(user.ID)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
integrationForm := form.NewIntegrationForm(request.Request())
|
|
||||||
integrationForm.Merge(integration)
|
|
||||||
|
|
||||||
if integration.FeverUsername != "" && c.store.HasDuplicateFeverUsername(user.ID, integration.FeverUsername) {
|
|
||||||
ctx.SetFlashErrorMessage(ctx.Translate("There is already someone else with the same Fever username!"))
|
|
||||||
response.Redirect(ctx.Route("integrations"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if integration.FeverEnabled {
|
|
||||||
integration.FeverToken = fmt.Sprintf("%x", md5.Sum([]byte(integration.FeverUsername+":"+integration.FeverPassword)))
|
|
||||||
} else {
|
|
||||||
integration.FeverToken = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.store.UpdateIntegration(integration)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Redirect(ctx.Route("integrations"))
|
|
||||||
}
|
|
80
ui/login.go
80
ui/login.go
|
@ -1,80 +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 (
|
|
||||||
"github.com/miniflux/miniflux/http/cookie"
|
|
||||||
"github.com/miniflux/miniflux/http/handler"
|
|
||||||
"github.com/miniflux/miniflux/logger"
|
|
||||||
"github.com/miniflux/miniflux/ui/form"
|
|
||||||
|
|
||||||
"github.com/tomasen/realip"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ShowLoginPage shows the login form.
|
|
||||||
func (c *Controller) ShowLoginPage(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
if ctx.IsAuthenticated() {
|
|
||||||
response.Redirect(ctx.Route("unread"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.HTML().Render("login", ctx.UserLanguage(), tplParams{
|
|
||||||
"csrf": ctx.CSRF(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckLogin validates the username/password and redirects the user to the unread page.
|
|
||||||
func (c *Controller) CheckLogin(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
authForm := form.NewAuthForm(request.Request())
|
|
||||||
tplParams := tplParams{
|
|
||||||
"errorMessage": "Invalid username or password.",
|
|
||||||
"csrf": ctx.CSRF(),
|
|
||||||
"form": authForm,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := authForm.Validate(); err != nil {
|
|
||||||
logger.Error("[Controller:CheckLogin] %v", err)
|
|
||||||
response.HTML().Render("login", ctx.UserLanguage(), tplParams)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.store.CheckPassword(authForm.Username, authForm.Password); err != nil {
|
|
||||||
logger.Error("[Controller:CheckLogin] %v", err)
|
|
||||||
response.HTML().Render("login", ctx.UserLanguage(), tplParams)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionToken, err := c.store.CreateUserSession(
|
|
||||||
authForm.Username,
|
|
||||||
request.Request().UserAgent(),
|
|
||||||
realip.RealIP(request.Request()),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("[Controller:CheckLogin] username=%s just logged in", authForm.Username)
|
|
||||||
|
|
||||||
response.SetCookie(cookie.New(cookie.CookieUserSessionID, sessionToken, c.cfg.IsHTTPS, c.cfg.BasePath()))
|
|
||||||
response.Redirect(ctx.Route("unread"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout destroy the session and redirects the user to the login page.
|
|
||||||
func (c *Controller) Logout(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
|
|
||||||
if err := c.store.UpdateSessionField(ctx.SessionID(), "language", user.Language); err != nil {
|
|
||||||
logger.Error("[Controller:Logout] %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.store.RemoveUserSessionByToken(user.ID, ctx.UserSessionToken()); err != nil {
|
|
||||||
logger.Error("[Controller:Logout] %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
response.SetCookie(cookie.Expired(cookie.CookieUserSessionID, c.cfg.IsHTTPS, c.cfg.BasePath()))
|
|
||||||
response.Redirect(ctx.Route("login"))
|
|
||||||
}
|
|
66
ui/login_check.go
Normal file
66
ui/login_check.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/miniflux/miniflux/http/context"
|
||||||
|
"github.com/miniflux/miniflux/http/cookie"
|
||||||
|
"github.com/miniflux/miniflux/http/response"
|
||||||
|
"github.com/miniflux/miniflux/http/response/html"
|
||||||
|
"github.com/miniflux/miniflux/http/route"
|
||||||
|
"github.com/miniflux/miniflux/logger"
|
||||||
|
"github.com/miniflux/miniflux/ui/form"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
"github.com/miniflux/miniflux/ui/view"
|
||||||
|
"github.com/tomasen/realip"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckLogin validates the username/password and redirects the user to the unread page.
|
||||||
|
func (c *Controller) CheckLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := context.New(r)
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
|
||||||
|
authForm := form.NewAuthForm(r)
|
||||||
|
|
||||||
|
view := view.New(c.tpl, ctx, sess)
|
||||||
|
view.Set("errorMessage", "Invalid username or password.")
|
||||||
|
view.Set("form", authForm)
|
||||||
|
|
||||||
|
if err := authForm.Validate(); err != nil {
|
||||||
|
logger.Error("[Controller:CheckLogin] %v", err)
|
||||||
|
html.OK(w, view.Render("login"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.store.CheckPassword(authForm.Username, authForm.Password); err != nil {
|
||||||
|
logger.Error("[Controller:CheckLogin] %v", err)
|
||||||
|
html.OK(w, view.Render("login"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionToken, userID, err := c.store.CreateUserSession(authForm.Username, r.UserAgent(), realip.RealIP(r))
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("[Controller:CheckLogin] username=%s just logged in", authForm.Username)
|
||||||
|
c.store.SetLastLogin(userID)
|
||||||
|
|
||||||
|
userLanguage, err := c.store.UserLanguage(userID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess.SetLanguage(userLanguage)
|
||||||
|
|
||||||
|
http.SetCookie(w, cookie.New(
|
||||||
|
cookie.CookieUserSessionID,
|
||||||
|
sessionToken,
|
||||||
|
c.cfg.IsHTTPS,
|
||||||
|
c.cfg.BasePath(),
|
||||||
|
))
|
||||||
|
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "unread"))
|
||||||
|
}
|
29
ui/login_show.go
Normal file
29
ui/login_show.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// 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 (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/miniflux/miniflux/http/context"
|
||||||
|
"github.com/miniflux/miniflux/http/response"
|
||||||
|
"github.com/miniflux/miniflux/http/response/html"
|
||||||
|
"github.com/miniflux/miniflux/http/route"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
"github.com/miniflux/miniflux/ui/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShowLoginPage shows the login form.
|
||||||
|
func (c *Controller) ShowLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := context.New(r)
|
||||||
|
if ctx.IsAuthenticated() {
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "unread"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
view := view.New(c.tpl, ctx, sess)
|
||||||
|
html.OK(w, view.Render("login"))
|
||||||
|
}
|
43
ui/logout.go
Normal file
43
ui/logout.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// 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/cookie"
|
||||||
|
"github.com/miniflux/miniflux/http/response"
|
||||||
|
"github.com/miniflux/miniflux/http/response/html"
|
||||||
|
"github.com/miniflux/miniflux/http/route"
|
||||||
|
"github.com/miniflux/miniflux/logger"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logout destroy the session and redirects the user to the login page.
|
||||||
|
func (c *Controller) Logout(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := context.New(r)
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
|
||||||
|
user, err := c.store.UserByID(ctx.UserID())
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess.SetLanguage(user.Language)
|
||||||
|
|
||||||
|
if err := c.store.RemoveUserSessionByToken(user.ID, ctx.UserSessionToken()); err != nil {
|
||||||
|
logger.Error("[Controller:Logout] %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, cookie.Expired(
|
||||||
|
cookie.CookieUserSessionID,
|
||||||
|
c.cfg.IsHTTPS,
|
||||||
|
c.cfg.BasePath(),
|
||||||
|
))
|
||||||
|
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||||
|
}
|
155
ui/oauth2.go
155
ui/oauth2.go
|
@ -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
|
// Use of this source code is governed by the Apache 2.0
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
@ -6,162 +6,9 @@ package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/miniflux/miniflux/config"
|
"github.com/miniflux/miniflux/config"
|
||||||
"github.com/miniflux/miniflux/http/cookie"
|
|
||||||
"github.com/miniflux/miniflux/http/handler"
|
|
||||||
"github.com/miniflux/miniflux/logger"
|
|
||||||
"github.com/miniflux/miniflux/model"
|
|
||||||
"github.com/miniflux/miniflux/oauth2"
|
"github.com/miniflux/miniflux/oauth2"
|
||||||
|
|
||||||
"github.com/tomasen/realip"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// OAuth2Redirect redirects the user to the consent page to ask for permission.
|
|
||||||
func (c *Controller) OAuth2Redirect(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
provider := request.StringParam("provider", "")
|
|
||||||
if provider == "" {
|
|
||||||
logger.Error("[OAuth2] Invalid or missing provider: %s", provider)
|
|
||||||
response.Redirect(ctx.Route("login"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[OAuth2] %v", err)
|
|
||||||
response.Redirect(ctx.Route("login"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Redirect(authProvider.GetRedirectURL(ctx.GenerateOAuth2State()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// OAuth2Callback receives the authorization code and create a new session.
|
|
||||||
func (c *Controller) OAuth2Callback(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
provider := request.StringParam("provider", "")
|
|
||||||
if provider == "" {
|
|
||||||
logger.Error("[OAuth2] Invalid or missing provider")
|
|
||||||
response.Redirect(ctx.Route("login"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
code := request.QueryStringParam("code", "")
|
|
||||||
if code == "" {
|
|
||||||
logger.Error("[OAuth2] No code received on callback")
|
|
||||||
response.Redirect(ctx.Route("login"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state := request.QueryStringParam("state", "")
|
|
||||||
if state == "" || state != ctx.OAuth2State() {
|
|
||||||
logger.Error(`[OAuth2] Invalid state value: got "%s" instead of "%s"`, state, ctx.OAuth2State())
|
|
||||||
response.Redirect(ctx.Route("login"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[OAuth2] %v", err)
|
|
||||||
response.Redirect(ctx.Route("login"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
profile, err := authProvider.GetProfile(code)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[OAuth2] %v", err)
|
|
||||||
response.Redirect(ctx.Route("login"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.IsAuthenticated() {
|
|
||||||
user, err := c.store.UserByExtraField(profile.Key, profile.ID)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if user != nil {
|
|
||||||
logger.Error("[OAuth2] User #%d cannot be associated because %s is already associated", ctx.UserID(), user.Username)
|
|
||||||
ctx.SetFlashErrorMessage(ctx.Translate("There is already someone associated with this provider!"))
|
|
||||||
response.Redirect(ctx.Route("settings"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user = ctx.LoggedUser()
|
|
||||||
if err := c.store.UpdateExtraField(user.ID, profile.Key, profile.ID); err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.SetFlashMessage(ctx.Translate("Your external account is now linked !"))
|
|
||||||
response.Redirect(ctx.Route("settings"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := c.store.UserByExtraField(profile.Key, profile.ID)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if user == nil {
|
|
||||||
if !c.cfg.IsOAuth2UserCreationAllowed() {
|
|
||||||
response.HTML().Forbidden()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user = model.NewUser()
|
|
||||||
user.Username = profile.Username
|
|
||||||
user.IsAdmin = false
|
|
||||||
user.Extra[profile.Key] = profile.ID
|
|
||||||
|
|
||||||
if err := c.store.CreateUser(user); err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionToken, err := c.store.CreateUserSession(
|
|
||||||
user.Username,
|
|
||||||
request.Request().UserAgent(),
|
|
||||||
realip.RealIP(request.Request()),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("[Controller:OAuth2Callback] username=%s just logged in", user.Username)
|
|
||||||
|
|
||||||
response.SetCookie(cookie.New(cookie.CookieUserSessionID, sessionToken, c.cfg.IsHTTPS, c.cfg.BasePath()))
|
|
||||||
response.Redirect(ctx.Route("unread"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// OAuth2Unlink unlink an account from the external provider.
|
|
||||||
func (c *Controller) OAuth2Unlink(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
provider := request.StringParam("provider", "")
|
|
||||||
if provider == "" {
|
|
||||||
logger.Info("[OAuth2] Invalid or missing provider")
|
|
||||||
response.Redirect(ctx.Route("login"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[OAuth2] %v", err)
|
|
||||||
response.Redirect(ctx.Route("settings"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
if err := c.store.RemoveExtraField(user.ID, authProvider.GetUserExtraKey()); err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Redirect(ctx.Route("settings"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOAuth2Manager(cfg *config.Config) *oauth2.Manager {
|
func getOAuth2Manager(cfg *config.Config) *oauth2.Manager {
|
||||||
return oauth2.NewManager(
|
return oauth2.NewManager(
|
||||||
cfg.OAuth2ClientID(),
|
cfg.OAuth2ClientID(),
|
||||||
|
|
128
ui/oauth2_callback.go
Normal file
128
ui/oauth2_callback.go
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
// 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 (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/miniflux/miniflux/http/context"
|
||||||
|
"github.com/miniflux/miniflux/http/cookie"
|
||||||
|
"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"
|
||||||
|
"github.com/miniflux/miniflux/logger"
|
||||||
|
"github.com/miniflux/miniflux/model"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
|
||||||
|
"github.com/tomasen/realip"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OAuth2Callback receives the authorization code and create a new session.
|
||||||
|
func (c *Controller) OAuth2Callback(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := context.New(r)
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
|
||||||
|
provider := request.Param(r, "provider", "")
|
||||||
|
if provider == "" {
|
||||||
|
logger.Error("[OAuth2] Invalid or missing provider")
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
code := request.QueryParam(r, "code", "")
|
||||||
|
if code == "" {
|
||||||
|
logger.Error("[OAuth2] No code received on callback")
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state := request.QueryParam(r, "state", "")
|
||||||
|
if state == "" || state != ctx.OAuth2State() {
|
||||||
|
logger.Error(`[OAuth2] Invalid state value: got "%s" instead of "%s"`, state, ctx.OAuth2State())
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[OAuth2] %v", err)
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profile, err := authProvider.GetProfile(code)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[OAuth2] %v", err)
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.IsAuthenticated() {
|
||||||
|
user, err := c.store.UserByExtraField(profile.Key, profile.ID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user != nil {
|
||||||
|
logger.Error("[OAuth2] User #%d cannot be associated because %s is already associated", ctx.UserID(), user.Username)
|
||||||
|
sess.NewFlashErrorMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("There is already someone associated with this provider!"))
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "settings"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.store.UpdateExtraField(ctx.UserID(), profile.Key, profile.ID); err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess.NewFlashMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("Your external account is now linked!"))
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "settings"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := c.store.UserByExtraField(profile.Key, profile.ID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
if !c.cfg.IsOAuth2UserCreationAllowed() {
|
||||||
|
html.Forbidden(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user = model.NewUser()
|
||||||
|
user.Username = profile.Username
|
||||||
|
user.IsAdmin = false
|
||||||
|
user.Extra[profile.Key] = profile.ID
|
||||||
|
|
||||||
|
if err := c.store.CreateUser(user); err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionToken, _, err := c.store.CreateUserSession(user.Username, r.UserAgent(), realip.RealIP(r))
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("[Controller:OAuth2Callback] username=%s just logged in", user.Username)
|
||||||
|
c.store.SetLastLogin(user.ID)
|
||||||
|
sess.SetLanguage(user.Language)
|
||||||
|
|
||||||
|
http.SetCookie(w, cookie.New(
|
||||||
|
cookie.CookieUserSessionID,
|
||||||
|
sessionToken,
|
||||||
|
c.cfg.IsHTTPS,
|
||||||
|
c.cfg.BasePath(),
|
||||||
|
))
|
||||||
|
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "unread"))
|
||||||
|
}
|
38
ui/oauth2_redirect.go
Normal file
38
ui/oauth2_redirect.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// 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 (
|
||||||
|
"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/route"
|
||||||
|
"github.com/miniflux/miniflux/logger"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OAuth2Redirect redirects the user to the consent page to ask for permission.
|
||||||
|
func (c *Controller) OAuth2Redirect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := context.New(r)
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
|
||||||
|
provider := request.Param(r, "provider", "")
|
||||||
|
if provider == "" {
|
||||||
|
logger.Error("[OAuth2] Invalid or missing provider: %s", provider)
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[OAuth2] %v", err)
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Redirect(w, r, authProvider.GetRedirectURL(sess.NewOAuth2State()))
|
||||||
|
}
|
45
ui/oauth2_unlink.go
Normal file
45
ui/oauth2_unlink.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// 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 (
|
||||||
|
"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"
|
||||||
|
"github.com/miniflux/miniflux/logger"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OAuth2Unlink unlink an account from the external provider.
|
||||||
|
func (c *Controller) OAuth2Unlink(w http.ResponseWriter, r *http.Request) {
|
||||||
|
provider := request.Param(r, "provider", "")
|
||||||
|
if provider == "" {
|
||||||
|
logger.Info("[OAuth2] Invalid or missing provider")
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[OAuth2] %v", err)
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "settings"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.New(r)
|
||||||
|
if err := c.store.RemoveExtraField(ctx.UserID(), authProvider.GetUserExtraKey()); err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
sess.NewFlashMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("Your external account is now dissociated!"))
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "settings"))
|
||||||
|
return
|
||||||
|
}
|
72
ui/opml.go
72
ui/opml.go
|
@ -1,72 +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 (
|
|
||||||
"github.com/miniflux/miniflux/http/handler"
|
|
||||||
"github.com/miniflux/miniflux/logger"
|
|
||||||
"github.com/miniflux/miniflux/reader/opml"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Export generates the OPML file.
|
|
||||||
func (c *Controller) Export(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
opml, err := opml.NewHandler(c.store).Export(user.ID)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.XML().Download("feeds.opml", opml)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import shows the import form.
|
|
||||||
func (c *Controller) Import(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("import", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"menu": "feeds",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploadOPML handles OPML file importation.
|
|
||||||
func (c *Controller) UploadOPML(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
file, fileHeader, err := request.File("file")
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[Controller:UploadOPML] %v", err)
|
|
||||||
response.Redirect(ctx.Route("import"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
logger.Info(
|
|
||||||
"[Controller:UploadOPML] User #%d uploaded this file: %s (%d bytes)",
|
|
||||||
user.ID,
|
|
||||||
fileHeader.Filename,
|
|
||||||
fileHeader.Size,
|
|
||||||
)
|
|
||||||
|
|
||||||
if impErr := opml.NewHandler(c.store).Import(user.ID, file); impErr != nil {
|
|
||||||
args, err := c.getCommonTemplateArgs(ctx)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.HTML().Render("import", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"errorMessage": impErr,
|
|
||||||
"menu": "feeds",
|
|
||||||
}))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Redirect(ctx.Route("feeds"))
|
|
||||||
}
|
|
26
ui/opml_export.go
Normal file
26
ui/opml_export.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// 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/http/response/xml"
|
||||||
|
"github.com/miniflux/miniflux/reader/opml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Export generates the OPML file.
|
||||||
|
func (c *Controller) Export(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := context.New(r)
|
||||||
|
opml, err := opml.NewHandler(c.store).Export(ctx.UserID())
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xml.Attachment(w, "feeds.opml", opml)
|
||||||
|
}
|
33
ui/opml_import.go
Normal file
33
ui/opml_import.go
Normal file
|
@ -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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Import shows the import form.
|
||||||
|
func (c *Controller) Import(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", "feeds")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
html.OK(w, view.Render("import"))
|
||||||
|
}
|
64
ui/opml_upload.go
Normal file
64
ui/opml_upload.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// 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"
|
||||||
|
"github.com/miniflux/miniflux/http/response/html"
|
||||||
|
"github.com/miniflux/miniflux/http/route"
|
||||||
|
"github.com/miniflux/miniflux/logger"
|
||||||
|
"github.com/miniflux/miniflux/reader/opml"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
"github.com/miniflux/miniflux/ui/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UploadOPML handles OPML file importation.
|
||||||
|
func (c *Controller) UploadOPML(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
|
||||||
|
}
|
||||||
|
|
||||||
|
file, fileHeader, err := r.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[Controller:UploadOPML] %v", err)
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "import"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
logger.Info(
|
||||||
|
"[Controller:UploadOPML] User #%d uploaded this file: %s (%d bytes)",
|
||||||
|
user.ID,
|
||||||
|
fileHeader.Filename,
|
||||||
|
fileHeader.Size,
|
||||||
|
)
|
||||||
|
|
||||||
|
sess := session.New(c.store, ctx)
|
||||||
|
view := view.New(c.tpl, ctx, sess)
|
||||||
|
view.Set("menu", "feeds")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
if fileHeader.Size == 0 {
|
||||||
|
view.Set("errorMessage", "This file is empty")
|
||||||
|
html.OK(w, view.Render("import"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if impErr := opml.NewHandler(c.store).Import(user.ID, file); impErr != nil {
|
||||||
|
view.Set("errorMessage", impErr)
|
||||||
|
html.OK(w, view.Render("import"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "feeds"))
|
||||||
|
}
|
|
@ -12,14 +12,15 @@ import (
|
||||||
"github.com/miniflux/miniflux/model"
|
"github.com/miniflux/miniflux/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func decodeEntryStatusPayload(data io.Reader) (entryIDs []int64, status string, err error) {
|
func decodeEntryStatusPayload(r io.ReadCloser) (entryIDs []int64, status string, err error) {
|
||||||
type payload struct {
|
type payload struct {
|
||||||
EntryIDs []int64 `json:"entry_ids"`
|
EntryIDs []int64 `json:"entry_ids"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var p payload
|
var p payload
|
||||||
decoder := json.NewDecoder(data)
|
decoder := json.NewDecoder(r)
|
||||||
|
defer r.Close()
|
||||||
if err = decoder.Decode(&p); err != nil {
|
if err = decoder.Decode(&p); err != nil {
|
||||||
return nil, "", fmt.Errorf("invalid JSON payload: %v", err)
|
return nil, "", fmt.Errorf("invalid JSON payload: %v", err)
|
||||||
}
|
}
|
||||||
|
|
23
ui/proxy.go
23
ui/proxy.go
|
@ -8,31 +8,34 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/miniflux/miniflux/crypto"
|
"github.com/miniflux/miniflux/crypto"
|
||||||
"github.com/miniflux/miniflux/http/client"
|
"github.com/miniflux/miniflux/http/client"
|
||||||
"github.com/miniflux/miniflux/http/handler"
|
"github.com/miniflux/miniflux/http/request"
|
||||||
|
"github.com/miniflux/miniflux/http/response"
|
||||||
|
"github.com/miniflux/miniflux/http/response/html"
|
||||||
"github.com/miniflux/miniflux/logger"
|
"github.com/miniflux/miniflux/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ImageProxy fetch an image from a remote server and sent it back to the browser.
|
// ImageProxy fetch an image from a remote server and sent it back to the browser.
|
||||||
func (c *Controller) ImageProxy(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
func (c *Controller) ImageProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
// If we receive a "If-None-Match" header we assume the image in stored in browser cache
|
// If we receive a "If-None-Match" header we assume the image in stored in browser cache
|
||||||
if request.Request().Header.Get("If-None-Match") != "" {
|
if r.Header.Get("If-None-Match") != "" {
|
||||||
response.NotModified()
|
response.NotModified(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
encodedURL := request.StringParam("encodedURL", "")
|
encodedURL := request.Param(r, "encodedURL", "")
|
||||||
if encodedURL == "" {
|
if encodedURL == "" {
|
||||||
response.HTML().BadRequest(errors.New("No URL provided"))
|
html.BadRequest(w, errors.New("No URL provided"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
decodedURL, err := base64.URLEncoding.DecodeString(encodedURL)
|
decodedURL, err := base64.URLEncoding.DecodeString(encodedURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.HTML().BadRequest(errors.New("Unable to decode this URL"))
|
html.BadRequest(w, errors.New("Unable to decode this URL"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,17 +43,17 @@ func (c *Controller) ImageProxy(ctx *handler.Context, request *handler.Request,
|
||||||
resp, err := clt.Get()
|
resp, err := clt.Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("[Controller:ImageProxy] %v", err)
|
logger.Error("[Controller:ImageProxy] %v", err)
|
||||||
response.HTML().NotFound()
|
html.NotFound(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.HasServerFailure() {
|
if resp.HasServerFailure() {
|
||||||
response.HTML().NotFound()
|
html.NotFound(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
body, _ := ioutil.ReadAll(resp.Body)
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
etag := crypto.HashFromBytes(body)
|
etag := crypto.HashFromBytes(body)
|
||||||
|
|
||||||
response.Cache(resp.ContentType, etag, body, 72*time.Hour)
|
response.Cache(w, r, resp.ContentType, etag, body, 72*time.Hour)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,51 +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 (
|
|
||||||
"github.com/miniflux/miniflux/http/handler"
|
|
||||||
"github.com/miniflux/miniflux/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ShowSessions shows the list of active user sessions.
|
|
||||||
func (c *Controller) ShowSessions(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
|
|
||||||
}
|
|
||||||
|
|
||||||
sessions, err := c.store.UserSessions(user.ID)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sessions.UseTimezone(user.Timezone)
|
|
||||||
response.HTML().Render("sessions", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"sessions": sessions,
|
|
||||||
"currentSessionToken": ctx.UserSessionToken(),
|
|
||||||
"menu": "settings",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveSession remove a user session.
|
|
||||||
func (c *Controller) RemoveSession(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
|
|
||||||
sessionID, err := request.IntegerParam("sessionID")
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().BadRequest(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.store.RemoveUserSessionByID(user.ID, sessionID)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[Controller:RemoveSession] %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Redirect(ctx.Route("sessions"))
|
|
||||||
}
|
|
62
ui/session/session.go
Normal file
62
ui/session/session.go
Normal file
|
@ -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 session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/miniflux/miniflux/crypto"
|
||||||
|
"github.com/miniflux/miniflux/http/context"
|
||||||
|
"github.com/miniflux/miniflux/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Session handles session data.
|
||||||
|
type Session struct {
|
||||||
|
store *storage.Storage
|
||||||
|
ctx *context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOAuth2State generates a new OAuth2 state and stores the value into the database.
|
||||||
|
func (s *Session) NewOAuth2State() string {
|
||||||
|
state := crypto.GenerateRandomString(32)
|
||||||
|
s.store.UpdateSessionField(s.ctx.SessionID(), "oauth2_state", state)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFlashMessage creates a new flash message.
|
||||||
|
func (s *Session) NewFlashMessage(message string) {
|
||||||
|
s.store.UpdateSessionField(s.ctx.SessionID(), "flash_message", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlashMessage returns the current flash message if any.
|
||||||
|
func (s *Session) FlashMessage() string {
|
||||||
|
message := s.ctx.FlashMessage()
|
||||||
|
if message != "" {
|
||||||
|
s.store.UpdateSessionField(s.ctx.SessionID(), "flash_message", "")
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFlashErrorMessage creates a new flash error message.
|
||||||
|
func (s *Session) NewFlashErrorMessage(message string) {
|
||||||
|
s.store.UpdateSessionField(s.ctx.SessionID(), "flash_error_message", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlashErrorMessage returns the last flash error message if any.
|
||||||
|
func (s *Session) FlashErrorMessage() string {
|
||||||
|
message := s.ctx.FlashErrorMessage()
|
||||||
|
if message != "" {
|
||||||
|
s.store.UpdateSessionField(s.ctx.SessionID(), "flash_error_message", "")
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLanguage updates language field in session.
|
||||||
|
func (s *Session) SetLanguage(language string) {
|
||||||
|
s.store.UpdateSessionField(s.ctx.SessionID(), "language", language)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new session handler.
|
||||||
|
func New(store *storage.Storage, ctx *context.Context) *Session {
|
||||||
|
return &Session{store, ctx}
|
||||||
|
}
|
44
ui/session_list.go
Normal file
44
ui/session_list.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// 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/ui/session"
|
||||||
|
"github.com/miniflux/miniflux/ui/view"
|
||||||
|
|
||||||
|
"github.com/miniflux/miniflux/http/context"
|
||||||
|
"github.com/miniflux/miniflux/http/response/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShowSessions shows the list of active user sessions.
|
||||||
|
func (c *Controller) ShowSessions(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
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions, err := c.store.UserSessions(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions.UseTimezone(user.Timezone)
|
||||||
|
|
||||||
|
view.Set("currentSessionToken", ctx.UserSessionToken())
|
||||||
|
view.Set("sessions", sessions)
|
||||||
|
view.Set("menu", "settings")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
html.OK(w, view.Render("sessions"))
|
||||||
|
}
|
34
ui/session_remove.go
Normal file
34
ui/session_remove.go
Normal file
|
@ -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 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"
|
||||||
|
"github.com/miniflux/miniflux/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RemoveSession remove a user session.
|
||||||
|
func (c *Controller) RemoveSession(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := context.New(r)
|
||||||
|
|
||||||
|
sessionID, err := request.IntParam(r, "sessionID")
|
||||||
|
if err != nil {
|
||||||
|
html.BadRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.store.RemoveUserSessionByID(ctx.UserID(), sessionID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[Controller:RemoveSession] %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "sessions"))
|
||||||
|
}
|
|
@ -1,96 +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 (
|
|
||||||
"github.com/miniflux/miniflux/http/handler"
|
|
||||||
"github.com/miniflux/miniflux/locale"
|
|
||||||
"github.com/miniflux/miniflux/logger"
|
|
||||||
"github.com/miniflux/miniflux/model"
|
|
||||||
"github.com/miniflux/miniflux/ui/form"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ShowSettings shows the settings page.
|
|
||||||
func (c *Controller) ShowSettings(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
|
|
||||||
args, err := c.getSettingsFormTemplateArgs(ctx, user, nil)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.HTML().Render("settings", ctx.UserLanguage(), args)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateSettings update the settings.
|
|
||||||
func (c *Controller) UpdateSettings(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
|
|
||||||
settingsForm := form.NewSettingsForm(request.Request())
|
|
||||||
args, err := c.getSettingsFormTemplateArgs(ctx, user, settingsForm)
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := settingsForm.Validate(); err != nil {
|
|
||||||
response.HTML().Render("settings", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"form": settingsForm,
|
|
||||||
"errorMessage": err.Error(),
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.store.AnotherUserExists(user.ID, settingsForm.Username) {
|
|
||||||
response.HTML().Render("settings", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"form": settingsForm,
|
|
||||||
"errorMessage": "This user already exists.",
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.store.UpdateUser(settingsForm.Merge(user))
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[Controller:UpdateSettings] %v", err)
|
|
||||||
response.HTML().Render("settings", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"form": settingsForm,
|
|
||||||
"errorMessage": "Unable to update this user.",
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.SetFlashMessage(ctx.Translate("Preferences saved!"))
|
|
||||||
response.Redirect(ctx.Route("settings"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) getSettingsFormTemplateArgs(ctx *handler.Context, user *model.User, settingsForm *form.SettingsForm) (tplParams, error) {
|
|
||||||
args, err := c.getCommonTemplateArgs(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return args, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if settingsForm == nil {
|
|
||||||
args["form"] = form.SettingsForm{
|
|
||||||
Username: user.Username,
|
|
||||||
Theme: user.Theme,
|
|
||||||
Language: user.Language,
|
|
||||||
Timezone: user.Timezone,
|
|
||||||
EntryDirection: user.EntryDirection,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
args["form"] = settingsForm
|
|
||||||
}
|
|
||||||
|
|
||||||
args["menu"] = "settings"
|
|
||||||
args["themes"] = model.Themes()
|
|
||||||
args["languages"] = locale.AvailableLanguages()
|
|
||||||
args["timezones"], err = c.store.Timezones()
|
|
||||||
if err != nil {
|
|
||||||
return args, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return args, nil
|
|
||||||
}
|
|
54
ui/settings_show.go
Normal file
54
ui/settings_show.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// 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/locale"
|
||||||
|
"github.com/miniflux/miniflux/model"
|
||||||
|
"github.com/miniflux/miniflux/ui/form"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
"github.com/miniflux/miniflux/ui/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShowSettings shows the settings page.
|
||||||
|
func (c *Controller) ShowSettings(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
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsForm := form.SettingsForm{
|
||||||
|
Username: user.Username,
|
||||||
|
Theme: user.Theme,
|
||||||
|
Language: user.Language,
|
||||||
|
Timezone: user.Timezone,
|
||||||
|
EntryDirection: user.EntryDirection,
|
||||||
|
}
|
||||||
|
|
||||||
|
timezones, err := c.store.Timezones()
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
view.Set("form", settingsForm)
|
||||||
|
view.Set("themes", model.Themes())
|
||||||
|
view.Set("languages", locale.AvailableLanguages())
|
||||||
|
view.Set("timezones", timezones)
|
||||||
|
view.Set("menu", "settings")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
html.OK(w, view.Render("settings"))
|
||||||
|
}
|
72
ui/settings_update.go
Normal file
72
ui/settings_update.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
// 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"
|
||||||
|
"github.com/miniflux/miniflux/http/response/html"
|
||||||
|
"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/ui/form"
|
||||||
|
"github.com/miniflux/miniflux/ui/session"
|
||||||
|
"github.com/miniflux/miniflux/ui/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateSettings update the settings.
|
||||||
|
func (c *Controller) UpdateSettings(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
|
||||||
|
}
|
||||||
|
|
||||||
|
timezones, err := c.store.Timezones()
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsForm := form.NewSettingsForm(r)
|
||||||
|
|
||||||
|
view.Set("form", settingsForm)
|
||||||
|
view.Set("themes", model.Themes())
|
||||||
|
view.Set("languages", locale.AvailableLanguages())
|
||||||
|
view.Set("timezones", timezones)
|
||||||
|
view.Set("menu", "settings")
|
||||||
|
view.Set("user", user)
|
||||||
|
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||||
|
|
||||||
|
if err := settingsForm.Validate(); err != nil {
|
||||||
|
view.Set("errorMessage", err.Error())
|
||||||
|
html.OK(w, view.Render("settings"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.store.AnotherUserExists(user.ID, settingsForm.Username) {
|
||||||
|
view.Set("errorMessage", "This user already exists.")
|
||||||
|
html.OK(w, view.Render("settings"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.store.UpdateUser(settingsForm.Merge(user))
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[Controller:UpdateSettings] %v", err)
|
||||||
|
view.Set("errorMessage", "Unable to update this user.")
|
||||||
|
html.OK(w, view.Render("settings"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess.NewFlashMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("Preferences saved!"))
|
||||||
|
response.Redirect(w, r, route.Path(c.router, "settings"))
|
||||||
|
}
|
|
@ -1,68 +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 (
|
|
||||||
"github.com/miniflux/miniflux/http/handler"
|
|
||||||
"github.com/miniflux/miniflux/logger"
|
|
||||||
"github.com/miniflux/miniflux/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ShowStarredPage renders the page with all starred entries.
|
|
||||||
func (c *Controller) ShowStarredPage(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
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
count, err := builder.CountEntries()
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().ServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.HTML().Render("starred", ctx.UserLanguage(), args.Merge(tplParams{
|
|
||||||
"entries": entries,
|
|
||||||
"total": count,
|
|
||||||
"pagination": c.getPagination(ctx.Route("starred"), count, offset),
|
|
||||||
"menu": "starred",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToggleBookmark handles Ajax request to toggle bookmark value.
|
|
||||||
func (c *Controller) ToggleBookmark(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
user := ctx.LoggedUser()
|
|
||||||
entryID, err := request.IntegerParam("entryID")
|
|
||||||
if err != nil {
|
|
||||||
response.HTML().BadRequest(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.store.ToggleBookmark(user.ID, entryID); err != nil {
|
|
||||||
logger.Error("[Controller:UpdateEntryStatus] %v", err)
|
|
||||||
response.JSON().ServerError(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.JSON().Standard("OK")
|
|
||||||
}
|
|
97
ui/static.go
97
ui/static.go
|
@ -1,97 +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 (
|
|
||||||
"encoding/base64"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/miniflux/miniflux/http/handler"
|
|
||||||
"github.com/miniflux/miniflux/logger"
|
|
||||||
"github.com/miniflux/miniflux/ui/static"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Stylesheet renders the CSS.
|
|
||||||
func (c *Controller) Stylesheet(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
stylesheet := request.StringParam("name", "white")
|
|
||||||
body := static.Stylesheets["common"]
|
|
||||||
etag := static.StylesheetsChecksums["common"]
|
|
||||||
|
|
||||||
if theme, found := static.Stylesheets[stylesheet]; found {
|
|
||||||
body += theme
|
|
||||||
etag += static.StylesheetsChecksums[stylesheet]
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Cache("text/css; charset=utf-8", etag, []byte(body), 48*time.Hour)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Javascript renders application client side code.
|
|
||||||
func (c *Controller) Javascript(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
response.Cache("text/javascript; charset=utf-8", static.JavascriptChecksums["app"], []byte(static.Javascript["app"]), 48*time.Hour)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Favicon renders the application favicon.
|
|
||||||
func (c *Controller) Favicon(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
blob, err := base64.StdEncoding.DecodeString(static.Binaries["favicon.ico"])
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[Controller:Favicon] %v", err)
|
|
||||||
response.HTML().NotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Cache("image/x-icon", static.BinariesChecksums["favicon.ico"], blob, 48*time.Hour)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppIcon returns application icons.
|
|
||||||
func (c *Controller) AppIcon(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
filename := request.StringParam("filename", "favicon.png")
|
|
||||||
encodedBlob, found := static.Binaries[filename]
|
|
||||||
if !found {
|
|
||||||
logger.Info("[Controller:AppIcon] This icon doesn't exists: %s", filename)
|
|
||||||
response.HTML().NotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
blob, err := base64.StdEncoding.DecodeString(encodedBlob)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("[Controller:AppIcon] %v", err)
|
|
||||||
response.HTML().NotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Cache("image/png", static.BinariesChecksums[filename], blob, 48*time.Hour)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebManifest renders web manifest file.
|
|
||||||
func (c *Controller) WebManifest(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
|
||||||
type webManifestIcon struct {
|
|
||||||
Source string `json:"src"`
|
|
||||||
Sizes string `json:"sizes"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type webManifest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
ShortName string `json:"short_name"`
|
|
||||||
StartURL string `json:"start_url"`
|
|
||||||
Icons []webManifestIcon `json:"icons"`
|
|
||||||
Display string `json:"display"`
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest := &webManifest{
|
|
||||||
Name: "Miniflux",
|
|
||||||
ShortName: "Miniflux",
|
|
||||||
Description: "Minimalist Feed Reader",
|
|
||||||
Display: "minimal-ui",
|
|
||||||
StartURL: ctx.Route("unread"),
|
|
||||||
Icons: []webManifestIcon{
|
|
||||||
webManifestIcon{Source: ctx.Route("appIcon", "filename", "touch-icon-ipad-retina.png"), Sizes: "144x144", Type: "image/png"},
|
|
||||||
webManifestIcon{Source: ctx.Route("appIcon", "filename", "touch-icon-iphone-retina.png"), Sizes: "114x114", Type: "image/png"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
response.JSON().Standard(manifest)
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue