From f49b42f70f902d4da1e0fa4080e99164b331b716 Mon Sep 17 00:00:00 2001 From: Frédéric Guillot Date: Sun, 29 Apr 2018 16:35:04 -0700 Subject: Use vanilla HTTP handlers (refactoring) --- api/category.go | 59 ++--- api/entry.go | 132 ++++++----- api/feed.go | 119 +++++----- api/icon.go | 22 +- api/payload.go | 30 ++- api/subscription.go | 15 +- api/user.go | 99 +++++---- daemon/routes.go | 183 ++++++++------- fever/fever.go | 141 ++++++------ http/context/context.go | 108 +++++++++ http/handler/context.go | 167 -------------- http/handler/handler.go | 54 ----- http/handler/html_response.go | 65 ------ http/handler/json_response.go | 111 --------- http/handler/request.go | 124 ----------- http/handler/response.go | 88 -------- http/handler/xml_response.go | 29 --- http/request/request.go | 90 ++++++++ http/response/html/html.go | 57 +++++ http/response/json/json.go | 107 +++++++++ http/response/response.go | 34 +++ http/response/xml/xml.go | 23 ++ locale/translations.go | 26 ++- locale/translations/de_DE.json | 2 +- locale/translations/fr_FR.json | 6 +- locale/translations/nl_NL.json | 2 +- locale/translations/pl_PL.json | 2 +- locale/translations/zh_CN.json | 2 +- middleware/app_session.go | 29 +-- middleware/basic_auth.go | 14 +- middleware/common_headers.go | 25 +++ middleware/fever.go | 12 +- middleware/logging.go | 1 + middleware/user_session.go | 15 +- storage/category.go | 3 +- storage/entry.go | 14 ++ storage/user_session.go | 12 +- template/common.go | 2 +- template/engine.go | 5 +- template/views.go | 2 +- ui/about.go | 31 ++- ui/bookmark_entries.go | 61 +++++ ui/category.go | 257 --------------------- ui/category_create.go | 33 +++ ui/category_edit.go | 58 +++++ ui/category_entries.go | 78 +++++++ ui/category_list.go | 41 ++++ ui/category_remove.go | 50 +++++ ui/category_save.go | 71 ++++++ ui/category_update.go | 79 +++++++ ui/controller.go | 44 +--- ui/entry.go | 494 ----------------------------------------- ui/entry_category.go | 98 ++++++++ ui/entry_feed.go | 98 ++++++++ ui/entry_prev_next.go | 36 +++ ui/entry_read.go | 81 +++++++ ui/entry_save.go | 54 +++++ ui/entry_scraper.go | 53 +++++ ui/entry_starred.go | 91 ++++++++ ui/entry_toggle_bookmark.go | 32 +++ ui/entry_unread.go | 92 ++++++++ ui/entry_update_status.go | 39 ++++ ui/feed.go | 236 -------------------- ui/feed_edit.go | 71 ++++++ ui/feed_entries.go | 78 +++++++ ui/feed_icon.go | 36 +++ ui/feed_list.go | 41 ++++ ui/feed_refresh.go | 48 ++++ ui/feed_remove.go | 32 +++ ui/feed_update.go | 80 +++++++ ui/history.go | 61 ----- ui/history_entries.go | 59 +++++ ui/history_flush.go | 25 +++ ui/icon.go | 33 --- ui/integration_show.go | 63 ++++++ ui/integration_update.go | 60 +++++ ui/integrations.go | 87 -------- ui/login.go | 80 ------- ui/login_check.go | 66 ++++++ ui/login_show.go | 29 +++ ui/logout.go | 43 ++++ ui/oauth2.go | 155 +------------ ui/oauth2_callback.go | 128 +++++++++++ ui/oauth2_redirect.go | 38 ++++ ui/oauth2_unlink.go | 45 ++++ ui/opml.go | 72 ------ ui/opml_export.go | 26 +++ ui/opml_import.go | 33 +++ ui/opml_upload.go | 64 ++++++ ui/payload.go | 5 +- ui/proxy.go | 23 +- ui/session.go | 51 ----- ui/session/session.go | 62 ++++++ ui/session_list.go | 44 ++++ ui/session_remove.go | 34 +++ ui/settings.go | 96 -------- ui/settings_show.go | 54 +++++ ui/settings_update.go | 72 ++++++ ui/starred.go | 68 ------ ui/static.go | 97 -------- ui/static_app_icon.go | 37 +++ ui/static_favicon.go | 28 +++ ui/static_javascript.go | 18 ++ ui/static_manifest.go | 44 ++++ ui/static_stylesheet.go | 28 +++ ui/subscription.go | 145 ------------ ui/subscription_add.go | 40 ++++ ui/subscription_bookmarklet.go | 45 ++++ ui/subscription_choose.go | 59 +++++ ui/subscription_submit.go | 89 ++++++++ ui/unread.go | 59 ----- ui/unread_entries.go | 63 ++++++ ui/unread_mark_all_read.go | 23 ++ ui/user.go | 239 -------------------- ui/user_create.go | 40 ++++ ui/user_edit.go | 64 ++++++ ui/user_list.go | 47 ++++ ui/user_remove.go | 55 +++++ ui/user_save.go | 65 ++++++ ui/user_update.go | 84 +++++++ ui/view/view.go | 39 ++++ 121 files changed, 4339 insertions(+), 3369 deletions(-) create mode 100644 http/context/context.go delete mode 100644 http/handler/context.go delete mode 100644 http/handler/handler.go delete mode 100644 http/handler/html_response.go delete mode 100644 http/handler/json_response.go delete mode 100644 http/handler/request.go delete mode 100644 http/handler/response.go delete mode 100644 http/handler/xml_response.go create mode 100644 http/request/request.go create mode 100644 http/response/html/html.go create mode 100644 http/response/json/json.go create mode 100644 http/response/response.go create mode 100644 http/response/xml/xml.go create mode 100644 middleware/common_headers.go create mode 100644 ui/bookmark_entries.go delete mode 100644 ui/category.go create mode 100644 ui/category_create.go create mode 100644 ui/category_edit.go create mode 100644 ui/category_entries.go create mode 100644 ui/category_list.go create mode 100644 ui/category_remove.go create mode 100644 ui/category_save.go create mode 100644 ui/category_update.go delete mode 100644 ui/entry.go create mode 100644 ui/entry_category.go create mode 100644 ui/entry_feed.go create mode 100644 ui/entry_prev_next.go create mode 100644 ui/entry_read.go create mode 100644 ui/entry_save.go create mode 100644 ui/entry_scraper.go create mode 100644 ui/entry_starred.go create mode 100644 ui/entry_toggle_bookmark.go create mode 100644 ui/entry_unread.go create mode 100644 ui/entry_update_status.go delete mode 100644 ui/feed.go create mode 100644 ui/feed_edit.go create mode 100644 ui/feed_entries.go create mode 100644 ui/feed_icon.go create mode 100644 ui/feed_list.go create mode 100644 ui/feed_refresh.go create mode 100644 ui/feed_remove.go create mode 100644 ui/feed_update.go delete mode 100644 ui/history.go create mode 100644 ui/history_entries.go create mode 100644 ui/history_flush.go delete mode 100644 ui/icon.go create mode 100644 ui/integration_show.go create mode 100644 ui/integration_update.go delete mode 100644 ui/integrations.go delete mode 100644 ui/login.go create mode 100644 ui/login_check.go create mode 100644 ui/login_show.go create mode 100644 ui/logout.go create mode 100644 ui/oauth2_callback.go create mode 100644 ui/oauth2_redirect.go create mode 100644 ui/oauth2_unlink.go delete mode 100644 ui/opml.go create mode 100644 ui/opml_export.go create mode 100644 ui/opml_import.go create mode 100644 ui/opml_upload.go delete mode 100644 ui/session.go create mode 100644 ui/session/session.go create mode 100644 ui/session_list.go create mode 100644 ui/session_remove.go delete mode 100644 ui/settings.go create mode 100644 ui/settings_show.go create mode 100644 ui/settings_update.go delete mode 100644 ui/starred.go delete mode 100644 ui/static.go create mode 100644 ui/static_app_icon.go create mode 100644 ui/static_favicon.go create mode 100644 ui/static_javascript.go create mode 100644 ui/static_manifest.go create mode 100644 ui/static_stylesheet.go delete mode 100644 ui/subscription.go create mode 100644 ui/subscription_add.go create mode 100644 ui/subscription_bookmarklet.go create mode 100644 ui/subscription_choose.go create mode 100644 ui/subscription_submit.go delete mode 100644 ui/unread.go create mode 100644 ui/unread_entries.go create mode 100644 ui/unread_mark_all_read.go delete mode 100644 ui/user.go create mode 100644 ui/user_create.go create mode 100644 ui/user_edit.go create mode 100644 ui/user_list.go create mode 100644 ui/user_remove.go create mode 100644 ui/user_save.go create mode 100644 ui/user_update.go create mode 100644 ui/view/view.go diff --git a/api/category.go b/api/category.go index 67c7e52..30b12f4 100644 --- a/api/category.go +++ b/api/category.go @@ -6,98 +6,105 @@ package api import ( "errors" + "net/http" - "github.com/miniflux/miniflux/http/handler" + "github.com/miniflux/miniflux/http/context" + "github.com/miniflux/miniflux/http/request" + "github.com/miniflux/miniflux/http/response/json" ) // CreateCategory is the API handler to create a new category. -func (c *Controller) CreateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) { - userID := ctx.UserID() - category, err := decodeCategoryPayload(request.Body()) +func (c *Controller) CreateCategory(w http.ResponseWriter, r *http.Request) { + category, err := decodeCategoryPayload(r.Body) if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } + ctx := context.New(r) + userID := ctx.UserID() category.UserID = userID if err := category.ValidateCategoryCreation(); err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } if c, err := c.store.CategoryByTitle(userID, category.Title); err != nil || c != nil { - response.JSON().BadRequest(errors.New("This category already exists")) + json.BadRequest(w, errors.New("This category already exists")) return } err = c.store.CreateCategory(category) if err != nil { - response.JSON().ServerError(errors.New("Unable to create this category")) + json.ServerError(w, errors.New("Unable to create this category")) return } - response.JSON().Created(category) + json.Created(w, category) } // UpdateCategory is the API handler to update a category. -func (c *Controller) UpdateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) { - categoryID, err := request.IntegerParam("categoryID") +func (c *Controller) UpdateCategory(w http.ResponseWriter, r *http.Request) { + categoryID, err := request.IntParam(r, "categoryID") if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } - category, err := decodeCategoryPayload(request.Body()) + category, err := decodeCategoryPayload(r.Body) if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } + ctx := context.New(r) category.UserID = ctx.UserID() category.ID = categoryID if err := category.ValidateCategoryModification(); err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } err = c.store.UpdateCategory(category) if err != nil { - response.JSON().ServerError(errors.New("Unable to update this category")) + json.ServerError(w, errors.New("Unable to update this category")) return } - response.JSON().Created(category) + json.Created(w, category) } // GetCategories is the API handler to get a list of categories for a given user. -func (c *Controller) GetCategories(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) GetCategories(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) categories, err := c.store.Categories(ctx.UserID()) if err != nil { - response.JSON().ServerError(errors.New("Unable to fetch categories")) + json.ServerError(w, errors.New("Unable to fetch categories")) return } - response.JSON().Standard(categories) + json.OK(w, categories) } // RemoveCategory is the API handler to remove a category. -func (c *Controller) RemoveCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) RemoveCategory(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) userID := ctx.UserID() - categoryID, err := request.IntegerParam("categoryID") + categoryID, err := request.IntParam(r, "categoryID") if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } if !c.store.CategoryExists(userID, categoryID) { - response.JSON().NotFound(errors.New("Category not found")) + json.NotFound(w, errors.New("Category not found")) return } if err := c.store.RemoveCategory(userID, categoryID); err != nil { - response.JSON().ServerError(errors.New("Unable to remove this category")) + json.ServerError(w, errors.New("Unable to remove this category")) return } - response.JSON().NoContent() + json.NoContent(w) } diff --git a/api/entry.go b/api/entry.go index 4152da8..390f9ec 100644 --- a/api/entry.go +++ b/api/entry.go @@ -6,107 +6,110 @@ package api import ( "errors" + "net/http" - "github.com/miniflux/miniflux/http/handler" + "github.com/miniflux/miniflux/http/context" + "github.com/miniflux/miniflux/http/request" + "github.com/miniflux/miniflux/http/response/json" "github.com/miniflux/miniflux/model" ) // GetFeedEntry is the API handler to get a single feed entry. -func (c *Controller) GetFeedEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) { - userID := ctx.UserID() - feedID, err := request.IntegerParam("feedID") +func (c *Controller) GetFeedEntry(w http.ResponseWriter, r *http.Request) { + feedID, err := request.IntParam(r, "feedID") if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } - entryID, err := request.IntegerParam("entryID") + entryID, err := request.IntParam(r, "entryID") if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } + ctx := context.New(r) + userID := ctx.UserID() + builder := c.store.NewEntryQueryBuilder(userID) builder.WithFeedID(feedID) builder.WithEntryID(entryID) entry, err := builder.GetEntry() if err != nil { - response.JSON().ServerError(errors.New("Unable to fetch this entry from the database")) + json.ServerError(w, errors.New("Unable to fetch this entry from the database")) return } if entry == nil { - response.JSON().NotFound(errors.New("Entry not found")) + json.NotFound(w, errors.New("Entry not found")) return } - response.JSON().Standard(entry) + json.OK(w, entry) } // GetEntry is the API handler to get a single entry. -func (c *Controller) GetEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) { - userID := ctx.UserID() - entryID, err := request.IntegerParam("entryID") +func (c *Controller) GetEntry(w http.ResponseWriter, r *http.Request) { + entryID, err := request.IntParam(r, "entryID") if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } - builder := c.store.NewEntryQueryBuilder(userID) + builder := c.store.NewEntryQueryBuilder(context.New(r).UserID()) builder.WithEntryID(entryID) entry, err := builder.GetEntry() if err != nil { - response.JSON().ServerError(errors.New("Unable to fetch this entry from the database")) + json.ServerError(w, errors.New("Unable to fetch this entry from the database")) return } if entry == nil { - response.JSON().NotFound(errors.New("Entry not found")) + json.NotFound(w, errors.New("Entry not found")) return } - response.JSON().Standard(entry) + json.OK(w, entry) } // GetFeedEntries is the API handler to get all feed entries. -func (c *Controller) GetFeedEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) { - userID := ctx.UserID() - feedID, err := request.IntegerParam("feedID") +func (c *Controller) GetFeedEntries(w http.ResponseWriter, r *http.Request) { + feedID, err := request.IntParam(r, "feedID") if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } - status := request.QueryStringParam("status", "") + status := request.QueryParam(r, "status", "") if status != "" { if err := model.ValidateEntryStatus(status); err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } } - order := request.QueryStringParam("order", model.DefaultSortingOrder) + order := request.QueryParam(r, "order", model.DefaultSortingOrder) if err := model.ValidateEntryOrder(order); err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } - direction := request.QueryStringParam("direction", model.DefaultSortingDirection) + direction := request.QueryParam(r, "direction", model.DefaultSortingDirection) if err := model.ValidateDirection(direction); err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } - limit := request.QueryIntegerParam("limit", 100) - offset := request.QueryIntegerParam("offset", 0) + limit := request.QueryIntParam(r, "limit", 100) + offset := request.QueryIntParam(r, "offset", 0) if err := model.ValidateRange(offset, limit); err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } - builder := c.store.NewEntryQueryBuilder(userID) + builder := c.store.NewEntryQueryBuilder(context.New(r).UserID()) builder.WithFeedID(feedID) builder.WithStatus(status) builder.WithOrder(order) @@ -116,51 +119,49 @@ func (c *Controller) GetFeedEntries(ctx *handler.Context, request *handler.Reque entries, err := builder.GetEntries() if err != nil { - response.JSON().ServerError(errors.New("Unable to fetch the list of entries")) + json.ServerError(w, errors.New("Unable to fetch the list of entries")) return } count, err := builder.CountEntries() if err != nil { - response.JSON().ServerError(errors.New("Unable to count the number of entries")) + json.ServerError(w, errors.New("Unable to count the number of entries")) return } - response.JSON().Standard(&entriesResponse{Total: count, Entries: entries}) + json.OK(w, &entriesResponse{Total: count, Entries: entries}) } // GetEntries is the API handler to fetch entries. -func (c *Controller) GetEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) { - userID := ctx.UserID() - - status := request.QueryStringParam("status", "") +func (c *Controller) GetEntries(w http.ResponseWriter, r *http.Request) { + status := request.QueryParam(r, "status", "") if status != "" { if err := model.ValidateEntryStatus(status); err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } } - order := request.QueryStringParam("order", model.DefaultSortingOrder) + order := request.QueryParam(r, "order", model.DefaultSortingOrder) if err := model.ValidateEntryOrder(order); err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } - direction := request.QueryStringParam("direction", model.DefaultSortingDirection) + direction := request.QueryParam(r, "direction", model.DefaultSortingDirection) if err := model.ValidateDirection(direction); err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } - limit := request.QueryIntegerParam("limit", 100) - offset := request.QueryIntegerParam("offset", 0) + limit := request.QueryIntParam(r, "limit", 100) + offset := request.QueryIntParam(r, "offset", 0) if err := model.ValidateRange(offset, limit); err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } - builder := c.store.NewEntryQueryBuilder(userID) + builder := c.store.NewEntryQueryBuilder(context.New(r).UserID()) builder.WithStatus(status) builder.WithOrder(order) builder.WithDirection(direction) @@ -169,55 +170,52 @@ func (c *Controller) GetEntries(ctx *handler.Context, request *handler.Request, entries, err := builder.GetEntries() if err != nil { - response.JSON().ServerError(errors.New("Unable to fetch the list of entries")) + json.ServerError(w, errors.New("Unable to fetch the list of entries")) return } count, err := builder.CountEntries() if err != nil { - response.JSON().ServerError(errors.New("Unable to count the number of entries")) + json.ServerError(w, errors.New("Unable to count the number of entries")) return } - response.JSON().Standard(&entriesResponse{Total: count, Entries: entries}) + json.OK(w, &entriesResponse{Total: count, Entries: entries}) } // SetEntryStatus is the API handler to change the status of entries. -func (c *Controller) SetEntryStatus(ctx *handler.Context, request *handler.Request, response *handler.Response) { - userID := ctx.UserID() - - entryIDs, status, err := decodeEntryStatusPayload(request.Body()) +func (c *Controller) SetEntryStatus(w http.ResponseWriter, r *http.Request) { + entryIDs, status, err := decodeEntryStatusPayload(r.Body) if err != nil { - response.JSON().BadRequest(errors.New("Invalid JSON payload")) + json.BadRequest(w, errors.New("Invalid JSON payload")) return } if err := model.ValidateEntryStatus(status); err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } - if err := c.store.SetEntriesStatus(userID, entryIDs, status); err != nil { - response.JSON().ServerError(errors.New("Unable to change entries status")) + if err := c.store.SetEntriesStatus(context.New(r).UserID(), entryIDs, status); err != nil { + json.ServerError(w, errors.New("Unable to change entries status")) return } - response.JSON().NoContent() + json.NoContent(w) } // ToggleBookmark is the API handler to toggle bookmark status. -func (c *Controller) ToggleBookmark(ctx *handler.Context, request *handler.Request, response *handler.Response) { - userID := ctx.UserID() - entryID, err := request.IntegerParam("entryID") +func (c *Controller) ToggleBookmark(w http.ResponseWriter, r *http.Request) { + entryID, err := request.IntParam(r, "entryID") if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } - if err := c.store.ToggleBookmark(userID, entryID); err != nil { - response.JSON().ServerError(errors.New("Unable to toggle bookmark value")) + if err := c.store.ToggleBookmark(context.New(r).UserID(), entryID); err != nil { + json.ServerError(w, errors.New("Unable to toggle bookmark value")) return } - response.JSON().NoContent() + json.NoContent(w) } diff --git a/api/feed.go b/api/feed.go index 16e3d7d..72eaa41 100644 --- a/api/feed.go +++ b/api/feed.go @@ -6,44 +6,49 @@ package api 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/http/response/xml" "github.com/miniflux/miniflux/reader/opml" - - "github.com/miniflux/miniflux/http/handler" ) // CreateFeed is the API handler to create a new feed. -func (c *Controller) CreateFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) { - userID := ctx.UserID() - feedURL, categoryID, err := decodeFeedCreationPayload(request.Body()) +func (c *Controller) CreateFeed(w http.ResponseWriter, r *http.Request) { + feedURL, categoryID, err := decodeFeedCreationPayload(r.Body) if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } if feedURL == "" { - response.JSON().BadRequest(errors.New("The feed_url is required")) + json.BadRequest(w, errors.New("The feed_url is required")) return } if categoryID <= 0 { - response.JSON().BadRequest(errors.New("The category_id is required")) + json.BadRequest(w, errors.New("The category_id is required")) return } + ctx := context.New(r) + userID := ctx.UserID() + if c.store.FeedURLExists(userID, feedURL) { - response.JSON().BadRequest(errors.New("This feed_url already exists")) + json.BadRequest(w, errors.New("This feed_url already exists")) return } if !c.store.CategoryExists(userID, categoryID) { - response.JSON().BadRequest(errors.New("This category_id doesn't exists or doesn't belongs to this user")) + json.BadRequest(w, errors.New("This category_id doesn't exists or doesn't belongs to this user")) return } feed, err := c.feedHandler.CreateFeed(userID, categoryID, feedURL, false) if err != nil { - response.JSON().ServerError(errors.New("Unable to create this feed")) + json.ServerError(w, errors.New("Unable to create this feed")) return } @@ -51,142 +56,146 @@ func (c *Controller) CreateFeed(ctx *handler.Context, request *handler.Request, FeedID int64 `json:"feed_id"` } - response.JSON().Created(&result{FeedID: feed.ID}) + json.Created(w, &result{FeedID: feed.ID}) } // RefreshFeed is the API handler to refresh a feed. -func (c *Controller) RefreshFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) { - userID := ctx.UserID() - feedID, err := request.IntegerParam("feedID") +func (c *Controller) RefreshFeed(w http.ResponseWriter, r *http.Request) { + feedID, err := request.IntParam(r, "feedID") if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } + ctx := context.New(r) + userID := ctx.UserID() + if !c.store.FeedExists(userID, feedID) { - response.JSON().NotFound(errors.New("Unable to find this feed")) + json.NotFound(w, errors.New("Unable to find this feed")) return } err = c.feedHandler.RefreshFeed(userID, feedID) if err != nil { - response.JSON().ServerError(errors.New("Unable to refresh this feed")) + json.ServerError(w, errors.New("Unable to refresh this feed")) return } - response.JSON().NoContent() + json.NoContent(w) } // UpdateFeed is the API handler that is used to update a feed. -func (c *Controller) UpdateFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) { - userID := ctx.UserID() - feedID, err := request.IntegerParam("feedID") +func (c *Controller) UpdateFeed(w http.ResponseWriter, r *http.Request) { + feedID, err := request.IntParam(r, "feedID") if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } - newFeed, err := decodeFeedModificationPayload(request.Body()) + newFeed, err := decodeFeedModificationPayload(r.Body) if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } + ctx := context.New(r) + userID := ctx.UserID() + if newFeed.Category != nil && newFeed.Category.ID != 0 && !c.store.CategoryExists(userID, newFeed.Category.ID) { - response.JSON().BadRequest(errors.New("This category_id doesn't exists or doesn't belongs to this user")) + json.BadRequest(w, errors.New("This category_id doesn't exists or doesn't belongs to this user")) return } originalFeed, err := c.store.FeedByID(userID, feedID) if err != nil { - response.JSON().NotFound(errors.New("Unable to find this feed")) + json.NotFound(w, errors.New("Unable to find this feed")) return } if originalFeed == nil { - response.JSON().NotFound(errors.New("Feed not found")) + json.NotFound(w, errors.New("Feed not found")) return } originalFeed.Merge(newFeed) if err := c.store.UpdateFeed(originalFeed); err != nil { - response.JSON().ServerError(errors.New("Unable to update this feed")) + json.ServerError(w, errors.New("Unable to update this feed")) return } originalFeed, err = c.store.FeedByID(userID, feedID) if err != nil { - response.JSON().ServerError(errors.New("Unable to fetch this feed")) + json.ServerError(w, errors.New("Unable to fetch this feed")) return } - response.JSON().Created(originalFeed) + json.Created(w, originalFeed) } // GetFeeds is the API handler that get all feeds that belongs to the given user. -func (c *Controller) GetFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) { - feeds, err := c.store.Feeds(ctx.UserID()) +func (c *Controller) GetFeeds(w http.ResponseWriter, r *http.Request) { + feeds, err := c.store.Feeds(context.New(r).UserID()) if err != nil { - response.JSON().ServerError(errors.New("Unable to fetch feeds from the database")) + json.ServerError(w, errors.New("Unable to fetch feeds from the database")) return } - response.JSON().Standard(feeds) + json.OK(w, feeds) } // 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) - - opml, err := opmlHandler.Export(ctx.LoggedUser().ID) + opml, err := opmlHandler.Export(context.New(r).UserID()) 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. -func (c *Controller) GetFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) { - userID := ctx.UserID() - feedID, err := request.IntegerParam("feedID") +func (c *Controller) GetFeed(w http.ResponseWriter, r *http.Request) { + feedID, err := request.IntParam(r, "feedID") if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } - feed, err := c.store.FeedByID(userID, feedID) + feed, err := c.store.FeedByID(context.New(r).UserID(), feedID) if err != nil { - response.JSON().ServerError(errors.New("Unable to fetch this feed")) + json.ServerError(w, errors.New("Unable to fetch this feed")) return } if feed == nil { - response.JSON().NotFound(errors.New("Feed not found")) + json.NotFound(w, errors.New("Feed not found")) return } - response.JSON().Standard(feed) + json.OK(w, feed) } // RemoveFeed is the API handler to remove a feed. -func (c *Controller) RemoveFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) { - userID := ctx.UserID() - feedID, err := request.IntegerParam("feedID") +func (c *Controller) RemoveFeed(w http.ResponseWriter, r *http.Request) { + feedID, err := request.IntParam(r, "feedID") if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } + ctx := context.New(r) + userID := ctx.UserID() + if !c.store.FeedExists(userID, feedID) { - response.JSON().NotFound(errors.New("Feed not found")) + json.NotFound(w, errors.New("Feed not found")) return } if err := c.store.RemoveFeed(userID, feedID); err != nil { - response.JSON().ServerError(errors.New("Unable to remove this feed")) + json.ServerError(w, errors.New("Unable to remove this feed")) return } - response.JSON().NoContent() + json.NoContent(w) } diff --git a/api/icon.go b/api/icon.go index 7734dbf..0f5aa1b 100644 --- a/api/icon.go +++ b/api/icon.go @@ -6,36 +6,38 @@ package api import ( "errors" + "net/http" - "github.com/miniflux/miniflux/http/handler" + "github.com/miniflux/miniflux/http/context" + "github.com/miniflux/miniflux/http/request" + "github.com/miniflux/miniflux/http/response/json" ) // FeedIcon returns a feed icon. -func (c *Controller) FeedIcon(ctx *handler.Context, request *handler.Request, response *handler.Response) { - userID := ctx.UserID() - feedID, err := request.IntegerParam("feedID") +func (c *Controller) FeedIcon(w http.ResponseWriter, r *http.Request) { + feedID, err := request.IntParam(r, "feedID") if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } if !c.store.HasIcon(feedID) { - response.JSON().NotFound(errors.New("This feed doesn't have any icon")) + json.NotFound(w, errors.New("This feed doesn't have any icon")) return } - icon, err := c.store.IconByFeedID(userID, feedID) + icon, err := c.store.IconByFeedID(context.New(r).UserID(), feedID) if err != nil { - response.JSON().ServerError(errors.New("Unable to fetch feed icon")) + json.ServerError(w, errors.New("Unable to fetch feed icon")) return } if icon == nil { - response.JSON().NotFound(errors.New("This feed doesn't have any icon")) + json.NotFound(w, errors.New("This feed doesn't have any icon")) return } - response.JSON().Standard(&feedIcon{ + json.OK(w, &feedIcon{ ID: icon.ID, MimeType: icon.MimeType, Data: icon.DataURL(), diff --git a/api/payload.go b/api/payload.go index 46c3b04..bf470eb 100644 --- a/api/payload.go +++ b/api/payload.go @@ -23,10 +23,11 @@ type entriesResponse struct { Entries model.Entries `json:"entries"` } -func decodeUserPayload(data io.Reader) (*model.User, error) { +func decodeUserPayload(r io.ReadCloser) (*model.User, error) { var user model.User - decoder := json.NewDecoder(data) + decoder := json.NewDecoder(r) + defer r.Close() if err := decoder.Decode(&user); err != nil { return nil, fmt.Errorf("Unable to decode user JSON object: %v", err) } @@ -34,13 +35,14 @@ func decodeUserPayload(data io.Reader) (*model.User, error) { return &user, nil } -func decodeURLPayload(data io.Reader) (string, error) { +func decodeURLPayload(r io.ReadCloser) (string, error) { type payload struct { URL string `json:"url"` } var p payload - decoder := json.NewDecoder(data) + decoder := json.NewDecoder(r) + defer r.Close() if err := decoder.Decode(&p); err != nil { return "", fmt.Errorf("invalid JSON payload: %v", err) } @@ -48,14 +50,15 @@ func decodeURLPayload(data io.Reader) (string, error) { return p.URL, nil } -func decodeEntryStatusPayload(data io.Reader) ([]int64, string, error) { +func decodeEntryStatusPayload(r io.ReadCloser) ([]int64, string, error) { type payload struct { EntryIDs []int64 `json:"entry_ids"` Status string `json:"status"` } var p payload - decoder := json.NewDecoder(data) + decoder := json.NewDecoder(r) + defer r.Close() if err := decoder.Decode(&p); err != nil { return nil, "", fmt.Errorf("invalid JSON payload: %v", err) } @@ -63,14 +66,15 @@ func decodeEntryStatusPayload(data io.Reader) ([]int64, string, error) { return p.EntryIDs, p.Status, nil } -func decodeFeedCreationPayload(data io.Reader) (string, int64, error) { +func decodeFeedCreationPayload(r io.ReadCloser) (string, int64, error) { type payload struct { FeedURL string `json:"feed_url"` CategoryID int64 `json:"category_id"` } var p payload - decoder := json.NewDecoder(data) + decoder := json.NewDecoder(r) + defer r.Close() if err := decoder.Decode(&p); err != nil { return "", 0, fmt.Errorf("invalid JSON payload: %v", err) } @@ -78,10 +82,11 @@ func decodeFeedCreationPayload(data io.Reader) (string, int64, error) { return p.FeedURL, p.CategoryID, nil } -func decodeFeedModificationPayload(data io.Reader) (*model.Feed, error) { +func decodeFeedModificationPayload(r io.ReadCloser) (*model.Feed, error) { var feed model.Feed - decoder := json.NewDecoder(data) + decoder := json.NewDecoder(r) + defer r.Close() if err := decoder.Decode(&feed); err != nil { return nil, fmt.Errorf("Unable to decode feed JSON object: %v", err) } @@ -89,10 +94,11 @@ func decodeFeedModificationPayload(data io.Reader) (*model.Feed, error) { return &feed, nil } -func decodeCategoryPayload(data io.Reader) (*model.Category, error) { +func decodeCategoryPayload(r io.ReadCloser) (*model.Category, error) { var category model.Category - decoder := json.NewDecoder(data) + decoder := json.NewDecoder(r) + defer r.Close() if err := decoder.Decode(&category); err != nil { return nil, fmt.Errorf("Unable to decode category JSON object: %v", err) } diff --git a/api/subscription.go b/api/subscription.go index 8a8feff..c858f0e 100644 --- a/api/subscription.go +++ b/api/subscription.go @@ -7,29 +7,30 @@ package api import ( "errors" "fmt" + "net/http" - "github.com/miniflux/miniflux/http/handler" + "github.com/miniflux/miniflux/http/response/json" "github.com/miniflux/miniflux/reader/subscription" ) // GetSubscriptions is the API handler to find subscriptions. -func (c *Controller) GetSubscriptions(ctx *handler.Context, request *handler.Request, response *handler.Response) { - websiteURL, err := decodeURLPayload(request.Body()) +func (c *Controller) GetSubscriptions(w http.ResponseWriter, r *http.Request) { + websiteURL, err := decodeURLPayload(r.Body) if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } subscriptions, err := subscription.FindSubscriptions(websiteURL) if err != nil { - response.JSON().ServerError(errors.New("Unable to discover subscriptions")) + json.ServerError(w, errors.New("Unable to discover subscriptions")) return } if subscriptions == nil { - response.JSON().NotFound(fmt.Errorf("No subscription found")) + json.NotFound(w, fmt.Errorf("No subscription found")) return } - response.JSON().Standard(subscriptions) + json.OK(w, subscriptions) } diff --git a/api/user.go b/api/user.go index 359a9f7..3cddb46 100644 --- a/api/user.go +++ b/api/user.go @@ -6,182 +6,191 @@ package api import ( "errors" + "net/http" - "github.com/miniflux/miniflux/http/handler" + "github.com/miniflux/miniflux/http/context" + "github.com/miniflux/miniflux/http/request" + "github.com/miniflux/miniflux/http/response/json" ) // CreateUser is the API handler to create a new user. -func (c *Controller) CreateUser(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) CreateUser(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) if !ctx.IsAdminUser() { - response.JSON().Forbidden() + json.Forbidden(w) return } - user, err := decodeUserPayload(request.Body()) + user, err := decodeUserPayload(r.Body) if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } if err := user.ValidateUserCreation(); err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } if c.store.UserExists(user.Username) { - response.JSON().BadRequest(errors.New("This user already exists")) + json.BadRequest(w, errors.New("This user already exists")) return } err = c.store.CreateUser(user) if err != nil { - response.JSON().ServerError(errors.New("Unable to create this user")) + json.ServerError(w, errors.New("Unable to create this user")) return } user.Password = "" - response.JSON().Created(user) + json.Created(w, user) } // UpdateUser is the API handler to update the given user. -func (c *Controller) UpdateUser(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) UpdateUser(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) if !ctx.IsAdminUser() { - response.JSON().Forbidden() + json.Forbidden(w) return } - userID, err := request.IntegerParam("userID") + userID, err := request.IntParam(r, "userID") if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } - user, err := decodeUserPayload(request.Body()) + user, err := decodeUserPayload(r.Body) if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } if err := user.ValidateUserModification(); err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } originalUser, err := c.store.UserByID(userID) if err != nil { - response.JSON().BadRequest(errors.New("Unable to fetch this user from the database")) + json.BadRequest(w, errors.New("Unable to fetch this user from the database")) return } if originalUser == nil { - response.JSON().NotFound(errors.New("User not found")) + json.NotFound(w, errors.New("User not found")) return } originalUser.Merge(user) if err = c.store.UpdateUser(originalUser); err != nil { - response.JSON().ServerError(errors.New("Unable to update this user")) + json.ServerError(w, errors.New("Unable to update this user")) return } - response.JSON().Created(originalUser) + json.Created(w, originalUser) } // Users is the API handler to get the list of users. -func (c *Controller) Users(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) Users(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) if !ctx.IsAdminUser() { - response.JSON().Forbidden() + json.Forbidden(w) return } users, err := c.store.Users() if err != nil { - response.JSON().ServerError(errors.New("Unable to fetch the list of users")) + json.ServerError(w, errors.New("Unable to fetch the list of users")) return } users.UseTimezone(ctx.UserTimezone()) - response.JSON().Standard(users) + json.OK(w, users) } // UserByID is the API handler to fetch the given user by the ID. -func (c *Controller) UserByID(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) UserByID(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) if !ctx.IsAdminUser() { - response.JSON().Forbidden() + json.Forbidden(w) return } - userID, err := request.IntegerParam("userID") + userID, err := request.IntParam(r, "userID") if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } user, err := c.store.UserByID(userID) if err != nil { - response.JSON().BadRequest(errors.New("Unable to fetch this user from the database")) + json.BadRequest(w, errors.New("Unable to fetch this user from the database")) return } if user == nil { - response.JSON().NotFound(errors.New("User not found")) + json.NotFound(w, errors.New("User not found")) return } user.UseTimezone(ctx.UserTimezone()) - response.JSON().Standard(user) + json.OK(w, user) } // UserByUsername is the API handler to fetch the given user by the username. -func (c *Controller) UserByUsername(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) UserByUsername(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) if !ctx.IsAdminUser() { - response.JSON().Forbidden() + json.Forbidden(w) return } - username := request.StringParam("username", "") + username := request.Param(r, "username", "") user, err := c.store.UserByUsername(username) if err != nil { - response.JSON().BadRequest(errors.New("Unable to fetch this user from the database")) + json.BadRequest(w, errors.New("Unable to fetch this user from the database")) return } if user == nil { - response.JSON().NotFound(errors.New("User not found")) + json.NotFound(w, errors.New("User not found")) return } - response.JSON().Standard(user) + json.OK(w, user) } // RemoveUser is the API handler to remove an existing user. -func (c *Controller) RemoveUser(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) RemoveUser(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) if !ctx.IsAdminUser() { - response.JSON().Forbidden() + json.Forbidden(w) return } - userID, err := request.IntegerParam("userID") + userID, err := request.IntParam(r, "userID") if err != nil { - response.JSON().BadRequest(err) + json.BadRequest(w, err) return } user, err := c.store.UserByID(userID) if err != nil { - response.JSON().ServerError(errors.New("Unable to fetch this user from the database")) + json.ServerError(w, errors.New("Unable to fetch this user from the database")) return } if user == nil { - response.JSON().NotFound(errors.New("User not found")) + json.NotFound(w, errors.New("User not found")) return } if err := c.store.RemoveUser(user.ID); err != nil { - response.JSON().BadRequest(errors.New("Unable to remove this user from the database")) + json.BadRequest(w, errors.New("Unable to remove this user from the database")) return } - response.JSON().NoContent() + json.NoContent(w) } diff --git a/daemon/routes.go b/daemon/routes.go index f11e6d6..00d2827 100644 --- a/daemon/routes.go +++ b/daemon/routes.go @@ -10,7 +10,6 @@ import ( "github.com/miniflux/miniflux/api" "github.com/miniflux/miniflux/config" "github.com/miniflux/miniflux/fever" - "github.com/miniflux/miniflux/http/handler" "github.com/miniflux/miniflux/locale" "github.com/miniflux/miniflux/middleware" "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 { router := mux.NewRouter() templateEngine := template.NewEngine(cfg, router, translator) - apiController := api.NewController(store, feedHandler) feverController := fever.NewController(store) - uiController := ui.NewController(cfg, store, pool, feedHandler) - - apiHandler := handler.NewHandler(cfg, store, router, templateEngine, translator) - feverHandler := handler.NewHandler(cfg, store, router, templateEngine, translator) - uiHandler := handler.NewHandler(cfg, store, router, templateEngine, translator) + uiController := ui.NewController(cfg, store, pool, feedHandler, templateEngine, translator, router) middleware := middleware.New(cfg, store, router) 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.Logging) + router.Use(middleware.CommonHeaders) router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) { 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.Use(middleware.FeverAuth) - feverRouter.Handle("/", feverHandler.Use(feverController.Handler)).Name("feverEndpoint") + feverRouter.HandleFunc("/", feverController.Handler).Name("feverEndpoint") apiRouter := router.PathPrefix("/v1").Subrouter() apiRouter.Use(middleware.BasicAuth) - apiRouter.Handle("/users", apiHandler.Use(apiController.CreateUser)).Methods("POST") - apiRouter.Handle("/users", apiHandler.Use(apiController.Users)).Methods("GET") - apiRouter.Handle("/users/{userID:[0-9]+}", apiHandler.Use(apiController.UserByID)).Methods("GET") - apiRouter.Handle("/users/{userID:[0-9]+}", apiHandler.Use(apiController.UpdateUser)).Methods("PUT") - apiRouter.Handle("/users/{userID:[0-9]+}", apiHandler.Use(apiController.RemoveUser)).Methods("DELETE") - apiRouter.Handle("/users/{username}", apiHandler.Use(apiController.UserByUsername)).Methods("GET") - apiRouter.Handle("/categories", apiHandler.Use(apiController.CreateCategory)).Methods("POST") - apiRouter.Handle("/categories", apiHandler.Use(apiController.GetCategories)).Methods("GET") - apiRouter.Handle("/categories/{categoryID}", apiHandler.Use(apiController.UpdateCategory)).Methods("PUT") - apiRouter.Handle("/categories/{categoryID}", apiHandler.Use(apiController.RemoveCategory)).Methods("DELETE") - apiRouter.Handle("/discover", apiHandler.Use(apiController.GetSubscriptions)).Methods("POST") - apiRouter.Handle("/feeds", apiHandler.Use(apiController.CreateFeed)).Methods("POST") - apiRouter.Handle("/feeds", apiHandler.Use(apiController.GetFeeds)).Methods("Get") - apiRouter.Handle("/feeds/{feedID}/refresh", apiHandler.Use(apiController.RefreshFeed)).Methods("PUT") - apiRouter.Handle("/feeds/{feedID}", apiHandler.Use(apiController.GetFeed)).Methods("GET") - apiRouter.Handle("/feeds/{feedID}", apiHandler.Use(apiController.UpdateFeed)).Methods("PUT") - apiRouter.Handle("/feeds/{feedID}", apiHandler.Use(apiController.RemoveFeed)).Methods("DELETE") - apiRouter.Handle("/feeds/{feedID}/icon", apiHandler.Use(apiController.FeedIcon)).Methods("GET") - apiRouter.Handle("/export", apiHandler.Use(apiController.Export)).Methods("GET") - apiRouter.Handle("/feeds/{feedID}/entries", apiHandler.Use(apiController.GetFeedEntries)).Methods("GET") - apiRouter.Handle("/feeds/{feedID}/entries/{entryID}", apiHandler.Use(apiController.GetFeedEntry)).Methods("GET") - apiRouter.Handle("/entries", apiHandler.Use(apiController.GetEntries)).Methods("GET") - apiRouter.Handle("/entries", apiHandler.Use(apiController.SetEntryStatus)).Methods("PUT") - apiRouter.Handle("/entries/{entryID}", apiHandler.Use(apiController.GetEntry)).Methods("GET") - apiRouter.Handle("/entries/{entryID}/bookmark", apiHandler.Use(apiController.ToggleBookmark)).Methods("PUT") + apiRouter.HandleFunc("/users", apiController.CreateUser).Methods("POST") + apiRouter.HandleFunc("/users", apiController.Users).Methods("GET") + apiRouter.HandleFunc("/users/{userID:[0-9]+}", apiController.UserByID).Methods("GET") + apiRouter.HandleFunc("/users/{userID:[0-9]+}", apiController.UpdateUser).Methods("PUT") + apiRouter.HandleFunc("/users/{userID:[0-9]+}", apiController.RemoveUser).Methods("DELETE") + apiRouter.HandleFunc("/users/{username}", apiController.UserByUsername).Methods("GET") + apiRouter.HandleFunc("/categories", apiController.CreateCategory).Methods("POST") + apiRouter.HandleFunc("/categories", apiController.GetCategories).Methods("GET") + apiRouter.HandleFunc("/categories/{categoryID}", apiController.UpdateCategory).Methods("PUT") + apiRouter.HandleFunc("/categories/{categoryID}", apiController.RemoveCategory).Methods("DELETE") + apiRouter.HandleFunc("/discover", apiController.GetSubscriptions).Methods("POST") + apiRouter.HandleFunc("/feeds", apiController.CreateFeed).Methods("POST") + apiRouter.HandleFunc("/feeds", apiController.GetFeeds).Methods("Get") + apiRouter.HandleFunc("/feeds/{feedID}/refresh", apiController.RefreshFeed).Methods("PUT") + apiRouter.HandleFunc("/feeds/{feedID}", apiController.GetFeed).Methods("GET") + apiRouter.HandleFunc("/feeds/{feedID}", apiController.UpdateFeed).Methods("PUT") + apiRouter.HandleFunc("/feeds/{feedID}", apiController.RemoveFeed).Methods("DELETE") + apiRouter.HandleFunc("/feeds/{feedID}/icon", apiController.FeedIcon).Methods("GET") + apiRouter.HandleFunc("/export", apiController.Export).Methods("GET") + apiRouter.HandleFunc("/feeds/{feedID}/entries", apiController.GetFeedEntries).Methods("GET") + apiRouter.HandleFunc("/feeds/{feedID}/entries/{entryID}", apiController.GetFeedEntry).Methods("GET") + apiRouter.HandleFunc("/entries", apiController.GetEntries).Methods("GET") + apiRouter.HandleFunc("/entries", apiController.SetEntryStatus).Methods("PUT") + apiRouter.HandleFunc("/entries/{entryID}", apiController.GetEntry).Methods("GET") + apiRouter.HandleFunc("/entries/{entryID}/bookmark", apiController.ToggleBookmark).Methods("PUT") uiRouter := router.NewRoute().Subrouter() uiRouter.Use(middleware.AppSession) uiRouter.Use(middleware.UserSession) - uiRouter.Handle("/stylesheets/{name}.css", uiHandler.Use(uiController.Stylesheet)).Name("stylesheet").Methods("GET") - uiRouter.Handle("/js", uiHandler.Use(uiController.Javascript)).Name("javascript").Methods("GET") - uiRouter.Handle("/favicon.ico", uiHandler.Use(uiController.Favicon)).Name("favicon").Methods("GET") - uiRouter.Handle("/icon/{filename}", uiHandler.Use(uiController.AppIcon)).Name("appIcon").Methods("GET") - uiRouter.Handle("/manifest.json", uiHandler.Use(uiController.WebManifest)).Name("webManifest").Methods("GET") - uiRouter.Handle("/subscribe", uiHandler.Use(uiController.AddSubscription)).Name("addSubscription").Methods("GET") - uiRouter.Handle("/subscribe", uiHandler.Use(uiController.SubmitSubscription)).Name("submitSubscription").Methods("POST") - uiRouter.Handle("/subscriptions", uiHandler.Use(uiController.ChooseSubscription)).Name("chooseSubscription").Methods("POST") - uiRouter.Handle("/mark-all-as-read", uiHandler.Use(uiController.MarkAllAsRead)).Name("markAllAsRead").Methods("GET") - uiRouter.Handle("/unread", uiHandler.Use(uiController.ShowUnreadPage)).Name("unread").Methods("GET") - uiRouter.Handle("/history", uiHandler.Use(uiController.ShowHistoryPage)).Name("history").Methods("GET") - uiRouter.Handle("/starred", uiHandler.Use(uiController.ShowStarredPage)).Name("starred").Methods("GET") - uiRouter.Handle("/feed/{feedID}/refresh", uiHandler.Use(uiController.RefreshFeed)).Name("refreshFeed").Methods("GET") - uiRouter.Handle("/feed/{feedID}/edit", uiHandler.Use(uiController.EditFeed)).Name("editFeed").Methods("GET") - uiRouter.Handle("/feed/{feedID}/remove", uiHandler.Use(uiController.RemoveFeed)).Name("removeFeed").Methods("POST") - uiRouter.Handle("/feed/{feedID}/update", uiHandler.Use(uiController.UpdateFeed)).Name("updateFeed").Methods("POST") - uiRouter.Handle("/feed/{feedID}/entries", uiHandler.Use(uiController.ShowFeedEntries)).Name("feedEntries").Methods("GET") - uiRouter.Handle("/feeds", uiHandler.Use(uiController.ShowFeedsPage)).Name("feeds").Methods("GET") - uiRouter.Handle("/feeds/refresh", uiHandler.Use(uiController.RefreshAllFeeds)).Name("refreshAllFeeds").Methods("GET") - uiRouter.Handle("/unread/entry/{entryID}", uiHandler.Use(uiController.ShowUnreadEntry)).Name("unreadEntry").Methods("GET") - uiRouter.Handle("/history/entry/{entryID}", uiHandler.Use(uiController.ShowReadEntry)).Name("readEntry").Methods("GET") - uiRouter.Handle("/history/flush", uiHandler.Use(uiController.FlushHistory)).Name("flushHistory").Methods("GET") - uiRouter.Handle("/feed/{feedID}/entry/{entryID}", uiHandler.Use(uiController.ShowFeedEntry)).Name("feedEntry").Methods("GET") - uiRouter.Handle("/category/{categoryID}/entry/{entryID}", uiHandler.Use(uiController.ShowCategoryEntry)).Name("categoryEntry").Methods("GET") - uiRouter.Handle("/starred/entry/{entryID}", uiHandler.Use(uiController.ShowStarredEntry)).Name("starredEntry").Methods("GET") - uiRouter.Handle("/entry/status", uiHandler.Use(uiController.UpdateEntriesStatus)).Name("updateEntriesStatus").Methods("POST") - uiRouter.Handle("/entry/save/{entryID}", uiHandler.Use(uiController.SaveEntry)).Name("saveEntry").Methods("POST") - uiRouter.Handle("/entry/download/{entryID}", uiHandler.Use(uiController.FetchContent)).Name("fetchContent").Methods("POST") - uiRouter.Handle("/entry/bookmark/{entryID}", uiHandler.Use(uiController.ToggleBookmark)).Name("toggleBookmark").Methods("POST") - uiRouter.Handle("/categories", uiHandler.Use(uiController.ShowCategories)).Name("categories").Methods("GET") - uiRouter.Handle("/category/create", uiHandler.Use(uiController.CreateCategory)).Name("createCategory").Methods("GET") - uiRouter.Handle("/category/save", uiHandler.Use(uiController.SaveCategory)).Name("saveCategory").Methods("POST") - uiRouter.Handle("/category/{categoryID}/entries", uiHandler.Use(uiController.ShowCategoryEntries)).Name("categoryEntries").Methods("GET") - uiRouter.Handle("/category/{categoryID}/edit", uiHandler.Use(uiController.EditCategory)).Name("editCategory").Methods("GET") - uiRouter.Handle("/category/{categoryID}/update", uiHandler.Use(uiController.UpdateCategory)).Name("updateCategory").Methods("POST") - uiRouter.Handle("/category/{categoryID}/remove", uiHandler.Use(uiController.RemoveCategory)).Name("removeCategory").Methods("POST") - uiRouter.Handle("/feed/icon/{iconID}", uiHandler.Use(uiController.ShowIcon)).Name("icon").Methods("GET") - uiRouter.Handle("/proxy/{encodedURL}", uiHandler.Use(uiController.ImageProxy)).Name("proxy").Methods("GET") - uiRouter.Handle("/users", uiHandler.Use(uiController.ShowUsers)).Name("users").Methods("GET") - uiRouter.Handle("/user/create", uiHandler.Use(uiController.CreateUser)).Name("createUser").Methods("GET") - uiRouter.Handle("/user/save", uiHandler.Use(uiController.SaveUser)).Name("saveUser").Methods("POST") - uiRouter.Handle("/users/{userID}/edit", uiHandler.Use(uiController.EditUser)).Name("editUser").Methods("GET") - uiRouter.Handle("/users/{userID}/update", uiHandler.Use(uiController.UpdateUser)).Name("updateUser").Methods("POST") - uiRouter.Handle("/users/{userID}/remove", uiHandler.Use(uiController.RemoveUser)).Name("removeUser").Methods("POST") - uiRouter.Handle("/about", uiHandler.Use(uiController.AboutPage)).Name("about").Methods("GET") - uiRouter.Handle("/settings", uiHandler.Use(uiController.ShowSettings)).Name("settings").Methods("GET") - uiRouter.Handle("/settings", uiHandler.Use(uiController.UpdateSettings)).Name("updateSettings").Methods("POST") - uiRouter.Handle("/bookmarklet", uiHandler.Use(uiController.Bookmarklet)).Name("bookmarklet").Methods("GET") - uiRouter.Handle("/integrations", uiHandler.Use(uiController.ShowIntegrations)).Name("integrations").Methods("GET") - uiRouter.Handle("/integration", uiHandler.Use(uiController.UpdateIntegration)).Name("updateIntegration").Methods("POST") - uiRouter.Handle("/sessions", uiHandler.Use(uiController.ShowSessions)).Name("sessions").Methods("GET") - uiRouter.Handle("/sessions/{sessionID}/remove", uiHandler.Use(uiController.RemoveSession)).Name("removeSession").Methods("POST") - uiRouter.Handle("/export", uiHandler.Use(uiController.Export)).Name("export").Methods("GET") - uiRouter.Handle("/import", uiHandler.Use(uiController.Import)).Name("import").Methods("GET") - uiRouter.Handle("/upload", uiHandler.Use(uiController.UploadOPML)).Name("uploadOPML").Methods("POST") - uiRouter.Handle("/oauth2/{provider}/unlink", uiHandler.Use(uiController.OAuth2Unlink)).Name("oauth2Unlink").Methods("GET") - uiRouter.Handle("/oauth2/{provider}/redirect", uiHandler.Use(uiController.OAuth2Redirect)).Name("oauth2Redirect").Methods("GET") - uiRouter.Handle("/oauth2/{provider}/callback", uiHandler.Use(uiController.OAuth2Callback)).Name("oauth2Callback").Methods("GET") - uiRouter.Handle("/login", uiHandler.Use(uiController.CheckLogin)).Name("checkLogin").Methods("POST") - uiRouter.Handle("/logout", uiHandler.Use(uiController.Logout)).Name("logout").Methods("GET") - uiRouter.Handle("/", uiHandler.Use(uiController.ShowLoginPage)).Name("login").Methods("GET") + uiRouter.HandleFunc("/stylesheets/{name}.css", uiController.Stylesheet).Name("stylesheet").Methods("GET") + uiRouter.HandleFunc("/js", uiController.Javascript).Name("javascript").Methods("GET") + uiRouter.HandleFunc("/favicon.ico", uiController.Favicon).Name("favicon").Methods("GET") + uiRouter.HandleFunc("/icon/{filename}", uiController.AppIcon).Name("appIcon").Methods("GET") + uiRouter.HandleFunc("/manifest.json", uiController.WebManifest).Name("webManifest").Methods("GET") + uiRouter.HandleFunc("/subscribe", uiController.AddSubscription).Name("addSubscription").Methods("GET") + uiRouter.HandleFunc("/subscribe", uiController.SubmitSubscription).Name("submitSubscription").Methods("POST") + uiRouter.HandleFunc("/subscriptions", uiController.ChooseSubscription).Name("chooseSubscription").Methods("POST") + uiRouter.HandleFunc("/mark-all-as-read", uiController.MarkAllAsRead).Name("markAllAsRead").Methods("GET") + uiRouter.HandleFunc("/unread", uiController.ShowUnreadPage).Name("unread").Methods("GET") + uiRouter.HandleFunc("/history", uiController.ShowHistoryPage).Name("history").Methods("GET") + uiRouter.HandleFunc("/starred", uiController.ShowStarredPage).Name("starred").Methods("GET") + uiRouter.HandleFunc("/feed/{feedID}/refresh", uiController.RefreshFeed).Name("refreshFeed").Methods("GET") + uiRouter.HandleFunc("/feed/{feedID}/edit", uiController.EditFeed).Name("editFeed").Methods("GET") + uiRouter.HandleFunc("/feed/{feedID}/remove", uiController.RemoveFeed).Name("removeFeed").Methods("POST") + uiRouter.HandleFunc("/feed/{feedID}/update", uiController.UpdateFeed).Name("updateFeed").Methods("POST") + uiRouter.HandleFunc("/feed/{feedID}/entries", uiController.ShowFeedEntries).Name("feedEntries").Methods("GET") + uiRouter.HandleFunc("/feeds", uiController.ShowFeedsPage).Name("feeds").Methods("GET") + uiRouter.HandleFunc("/feeds/refresh", uiController.RefreshAllFeeds).Name("refreshAllFeeds").Methods("GET") + uiRouter.HandleFunc("/unread/entry/{entryID}", uiController.ShowUnreadEntry).Name("unreadEntry").Methods("GET") + uiRouter.HandleFunc("/history/entry/{entryID}", uiController.ShowReadEntry).Name("readEntry").Methods("GET") + uiRouter.HandleFunc("/history/flush", uiController.FlushHistory).Name("flushHistory").Methods("GET") + uiRouter.HandleFunc("/feed/{feedID}/entry/{entryID}", uiController.ShowFeedEntry).Name("feedEntry").Methods("GET") + uiRouter.HandleFunc("/category/{categoryID}/entry/{entryID}", uiController.ShowCategoryEntry).Name("categoryEntry").Methods("GET") + uiRouter.HandleFunc("/starred/entry/{entryID}", uiController.ShowStarredEntry).Name("starredEntry").Methods("GET") + uiRouter.HandleFunc("/entry/status", uiController.UpdateEntriesStatus).Name("updateEntriesStatus").Methods("POST") + uiRouter.HandleFunc("/entry/save/{entryID}", uiController.SaveEntry).Name("saveEntry").Methods("POST") + uiRouter.HandleFunc("/entry/download/{entryID}", uiController.FetchContent).Name("fetchContent").Methods("POST") + uiRouter.HandleFunc("/entry/bookmark/{entryID}", uiController.ToggleBookmark).Name("toggleBookmark").Methods("POST") + uiRouter.HandleFunc("/categories", uiController.CategoryList).Name("categories").Methods("GET") + uiRouter.HandleFunc("/category/create", uiController.CreateCategory).Name("createCategory").Methods("GET") + uiRouter.HandleFunc("/category/save", uiController.SaveCategory).Name("saveCategory").Methods("POST") + uiRouter.HandleFunc("/category/{categoryID}/entries", uiController.CategoryEntries).Name("categoryEntries").Methods("GET") + uiRouter.HandleFunc("/category/{categoryID}/edit", uiController.EditCategory).Name("editCategory").Methods("GET") + uiRouter.HandleFunc("/category/{categoryID}/update", uiController.UpdateCategory).Name("updateCategory").Methods("POST") + uiRouter.HandleFunc("/category/{categoryID}/remove", uiController.RemoveCategory).Name("removeCategory").Methods("POST") + uiRouter.HandleFunc("/feed/icon/{iconID}", uiController.ShowIcon).Name("icon").Methods("GET") + uiRouter.HandleFunc("/proxy/{encodedURL}", uiController.ImageProxy).Name("proxy").Methods("GET") + uiRouter.HandleFunc("/users", uiController.ShowUsers).Name("users").Methods("GET") + uiRouter.HandleFunc("/user/create", uiController.CreateUser).Name("createUser").Methods("GET") + uiRouter.HandleFunc("/user/save", uiController.SaveUser).Name("saveUser").Methods("POST") + uiRouter.HandleFunc("/users/{userID}/edit", uiController.EditUser).Name("editUser").Methods("GET") + uiRouter.HandleFunc("/users/{userID}/update", uiController.UpdateUser).Name("updateUser").Methods("POST") + uiRouter.HandleFunc("/users/{userID}/remove", uiController.RemoveUser).Name("removeUser").Methods("POST") + uiRouter.HandleFunc("/about", uiController.About).Name("about").Methods("GET") + uiRouter.HandleFunc("/settings", uiController.ShowSettings).Name("settings").Methods("GET") + uiRouter.HandleFunc("/settings", uiController.UpdateSettings).Name("updateSettings").Methods("POST") + uiRouter.HandleFunc("/bookmarklet", uiController.Bookmarklet).Name("bookmarklet").Methods("GET") + uiRouter.HandleFunc("/integrations", uiController.ShowIntegrations).Name("integrations").Methods("GET") + uiRouter.HandleFunc("/integration", uiController.UpdateIntegration).Name("updateIntegration").Methods("POST") + uiRouter.HandleFunc("/sessions", uiController.ShowSessions).Name("sessions").Methods("GET") + uiRouter.HandleFunc("/sessions/{sessionID}/remove", uiController.RemoveSession).Name("removeSession").Methods("POST") + uiRouter.HandleFunc("/export", uiController.Export).Name("export").Methods("GET") + uiRouter.HandleFunc("/import", uiController.Import).Name("import").Methods("GET") + uiRouter.HandleFunc("/upload", uiController.UploadOPML).Name("uploadOPML").Methods("POST") + uiRouter.HandleFunc("/oauth2/{provider}/unlink", uiController.OAuth2Unlink).Name("oauth2Unlink").Methods("GET") + uiRouter.HandleFunc("/oauth2/{provider}/redirect", uiController.OAuth2Redirect).Name("oauth2Redirect").Methods("GET") + uiRouter.HandleFunc("/oauth2/{provider}/callback", uiController.OAuth2Callback).Name("oauth2Callback").Methods("GET") + uiRouter.HandleFunc("/login", uiController.CheckLogin).Name("checkLogin").Methods("POST") + uiRouter.HandleFunc("/logout", uiController.Logout).Name("logout").Methods("GET") + uiRouter.HandleFunc("/", uiController.ShowLoginPage).Name("login").Methods("GET") return router } diff --git a/fever/fever.go b/fever/fever.go index 5131127..f7e366a 100644 --- a/fever/fever.go +++ b/fever/fever.go @@ -5,11 +5,14 @@ package fever import ( + "net/http" "strconv" "strings" "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/logger" "github.com/miniflux/miniflux/model" @@ -129,28 +132,28 @@ type Controller struct { } // Handler handles Fever API calls -func (c *Controller) Handler(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) Handler(w http.ResponseWriter, r *http.Request) { switch { - case request.HasQueryParam("groups"): - c.handleGroups(ctx, request, response) - case request.HasQueryParam("feeds"): - c.handleFeeds(ctx, request, response) - case request.HasQueryParam("favicons"): - c.handleFavicons(ctx, request, response) - case request.HasQueryParam("unread_item_ids"): - c.handleUnreadItems(ctx, request, response) - case request.HasQueryParam("saved_item_ids"): - c.handleSavedItems(ctx, request, response) - case request.HasQueryParam("items"): - c.handleItems(ctx, request, response) - case request.FormValue("mark") == "item": - c.handleWriteItems(ctx, request, response) - case request.FormValue("mark") == "feed": - c.handleWriteFeeds(ctx, request, response) - case request.FormValue("mark") == "group": - c.handleWriteGroups(ctx, request, response) + case request.HasQueryParam(r, "groups"): + c.handleGroups(w, r) + case request.HasQueryParam(r, "feeds"): + c.handleFeeds(w, r) + case request.HasQueryParam(r, "favicons"): + c.handleFavicons(w, r) + case request.HasQueryParam(r, "unread_item_ids"): + c.handleUnreadItems(w, r) + case request.HasQueryParam(r, "saved_item_ids"): + c.handleSavedItems(w, r) + case request.HasQueryParam(r, "items"): + c.handleItems(w, r) + case r.FormValue("mark") == "item": + c.handleWriteItems(w, r) + case r.FormValue("mark") == "feed": + c.handleWriteFeeds(w, r) + case r.FormValue("mark") == "group": + c.handleWriteGroups(w, r) default: - response.JSON().Standard(newBaseResponse()) + json.OK(w, newBaseResponse()) } } @@ -174,19 +177,20 @@ The “Sparks” super group is not included in this response and is composed of is_spark equal to 1. */ -func (c *Controller) handleGroups(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) handleGroups(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) userID := ctx.UserID() logger.Debug("[Fever] Fetching groups for userID=%d", userID) categories, err := c.store.Categories(userID) if err != nil { - response.JSON().ServerError(err) + json.ServerError(w, err) return } feeds, err := c.store.Feeds(userID) if err != nil { - response.JSON().ServerError(err) + json.ServerError(w, err) return } @@ -197,7 +201,7 @@ func (c *Controller) handleGroups(ctx *handler.Context, request *handler.Request result.FeedsGroups = c.buildFeedGroups(feeds) result.SetCommonValues() - response.JSON().Standard(result) + json.OK(w, result) } /* @@ -224,13 +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. */ -func (c *Controller) handleFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) handleFeeds(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) userID := ctx.UserID() logger.Debug("[Fever] Fetching feeds for userID=%d", userID) feeds, err := c.store.Feeds(userID) if err != nil { - response.JSON().ServerError(err) + json.ServerError(w, err) return } @@ -255,7 +260,7 @@ func (c *Controller) handleFeeds(ctx *handler.Context, request *handler.Request, result.FeedsGroups = c.buildFeedGroups(feeds) result.SetCommonValues() - response.JSON().Standard(result) + json.OK(w, result) } /* @@ -277,13 +282,14 @@ A PHP/HTML example: echo ''; */ -func (c *Controller) handleFavicons(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) handleFavicons(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) userID := ctx.UserID() logger.Debug("[Fever] Fetching favicons for userID=%d", userID) icons, err := c.store.Icons(userID) if err != nil { - response.JSON().ServerError(err) + json.ServerError(w, err) return } @@ -296,7 +302,7 @@ func (c *Controller) handleFavicons(ctx *handler.Context, request *handler.Reque } 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) */ -func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) handleItems(w http.ResponseWriter, r *http.Request) { var result itemsResponse + ctx := context.New(r) userID := ctx.UserID() logger.Debug("[Fever] Fetching items for userID=%d", userID) @@ -342,17 +349,17 @@ func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request, builder.WithOrder("id") builder.WithDirection(model.DefaultSortingDirection) - sinceID := request.QueryIntegerParam("since_id", 0) + sinceID := request.QueryIntParam(r, "since_id", 0) if sinceID > 0 { builder.WithGreaterThanEntryID(int64(sinceID)) } - maxID := request.QueryIntegerParam("max_id", 0) + maxID := request.QueryIntParam(r, "max_id", 0) if maxID > 0 { builder.WithOffset(maxID) } - csvItemIDs := request.QueryStringParam("with_ids", "") + csvItemIDs := request.QueryParam(r, "with_ids", "") if csvItemIDs != "" { var itemIDs []int64 @@ -367,7 +374,7 @@ func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request, entries, err := builder.GetEntries() if err != nil { - response.JSON().ServerError(err) + json.ServerError(w, err) return } @@ -375,7 +382,7 @@ func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request, builder.WithoutStatus(model.EntryStatusRemoved) result.Total, err = builder.CountEntries() if err != nil { - response.JSON().ServerError(err) + json.ServerError(w, err) return } @@ -404,7 +411,7 @@ func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request, } 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: unread_item_ids (string/comma-separated list of positive integers) */ -func (c *Controller) handleUnreadItems(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) handleUnreadItems(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) userID := ctx.UserID() logger.Debug("[Fever] Fetching unread items for userID=%d", userID) @@ -422,7 +430,7 @@ func (c *Controller) handleUnreadItems(ctx *handler.Context, request *handler.Re builder.WithStatus(model.EntryStatusUnread) entries, err := builder.GetEntries() if err != nil { - response.JSON().ServerError(err) + json.ServerError(w, err) return } @@ -434,7 +442,7 @@ func (c *Controller) handleUnreadItems(ctx *handler.Context, request *handler.Re var result unreadResponse result.ItemIDs = strings.Join(itemIDs, ",") result.SetCommonValues() - response.JSON().Standard(result) + json.OK(w, result) } /* @@ -445,7 +453,8 @@ with the remote Fever installation. saved_item_ids (string/comma-separated list of positive integers) */ -func (c *Controller) handleSavedItems(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) handleSavedItems(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) userID := ctx.UserID() logger.Debug("[Fever] Fetching saved items for userID=%d", userID) @@ -454,7 +463,7 @@ func (c *Controller) handleSavedItems(ctx *handler.Context, request *handler.Req entryIDs, err := builder.GetEntryIDs() if err != nil { - response.JSON().ServerError(err) + json.ServerError(w, err) return } @@ -465,7 +474,7 @@ func (c *Controller) handleSavedItems(ctx *handler.Context, request *handler.Req result := &savedResponse{ItemIDs: strings.Join(itemsIDs, ",")} result.SetCommonValues() - response.JSON().Standard(result) + json.OK(w, result) } /* @@ -473,11 +482,12 @@ func (c *Controller) handleSavedItems(ctx *handler.Context, request *handler.Req as=? where ? is replaced with read, saved or unsaved id=? where ? is replaced with the id of the item to modify */ -func (c *Controller) handleWriteItems(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) handleWriteItems(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) userID := ctx.UserID() logger.Debug("[Fever] Receiving mark=item call for userID=%d", userID) - entryID := request.FormIntegerValue("id") + entryID := request.FormIntValue(r, "id") if entryID <= 0 { return } @@ -488,7 +498,7 @@ func (c *Controller) handleWriteItems(ctx *handler.Context, request *handler.Req entry, err := builder.GetEntry() if err != nil { - response.JSON().ServerError(err) + json.ServerError(w, err) return } @@ -496,20 +506,23 @@ func (c *Controller) handleWriteItems(ctx *handler.Context, request *handler.Req return } - switch request.FormValue("as") { + switch r.FormValue("as") { case "read": + logger.Debug("[Fever] Mark entry #%d as read", entryID) c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead) case "unread": + logger.Debug("[Fever] Mark entry #%d as unread", entryID) c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread) case "saved", "unsaved": + logger.Debug("[Fever] Mark entry #%d as saved/unsaved", entryID) if err := c.store.ToggleBookmark(userID, entryID); err != nil { - response.JSON().ServerError(err) + json.ServerError(w, err) return } settings, err := c.store.Integration(userID) if err != nil { - response.JSON().ServerError(err) + json.ServerError(w, err) return } @@ -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 before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request */ -func (c *Controller) handleWriteFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) handleWriteFeeds(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) userID := ctx.UserID() logger.Debug("[Fever] Receiving mark=feed call for userID=%d", userID) - feedID := request.FormIntegerValue("id") + feedID := request.FormIntValue(r, "id") if feedID <= 0 { return } @@ -540,7 +554,7 @@ func (c *Controller) handleWriteFeeds(ctx *handler.Context, request *handler.Req builder.WithStatus(model.EntryStatusUnread) builder.WithFeedID(feedID) - before := request.FormIntegerValue("before") + before := request.FormIntValue(r, "before") if before > 0 { t := time.Unix(before, 0) builder.Before(&t) @@ -548,17 +562,17 @@ func (c *Controller) handleWriteFeeds(ctx *handler.Context, request *handler.Req entryIDs, err := builder.GetEntryIDs() if err != nil { - response.JSON().ServerError(err) + json.ServerError(w, err) return } err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead) if err != nil { - response.JSON().ServerError(err) + json.ServerError(w, err) return } - response.JSON().Standard(newBaseResponse()) + json.OK(w, newBaseResponse()) } /* @@ -567,11 +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 before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request */ -func (c *Controller) handleWriteGroups(ctx *handler.Context, request *handler.Request, response *handler.Response) { +func (c *Controller) handleWriteGroups(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) userID := ctx.UserID() logger.Debug("[Fever] Receiving mark=group call for userID=%d", userID) - groupID := request.FormIntegerValue("id") + groupID := request.FormIntValue(r, "id") if groupID < 0 { return } @@ -580,7 +595,7 @@ func (c *Controller) handleWriteGroups(ctx *handler.Context, request *handler.Re builder.WithStatus(model.EntryStatusUnread) builder.WithCategoryID(groupID) - before := request.FormIntegerValue("before") + before := request.FormIntValue(r, "before") if before > 0 { t := time.Unix(before, 0) builder.Before(&t) @@ -588,17 +603,17 @@ func (c *Controller) handleWriteGroups(ctx *handler.Context, request *handler.Re entryIDs, err := builder.GetEntryIDs() if err != nil { - response.JSON().ServerError(err) + json.ServerError(w, err) return } err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead) if err != nil { - response.JSON().ServerError(err) + json.ServerError(w, err) return } - response.JSON().Standard(newBaseResponse()) + json.OK(w, newBaseResponse()) } /* diff --git a/http/context/context.go b/http/context/context.go new file mode 100644 index 0000000..cf7cb09 --- /dev/null +++ b/http/context/context.go @@ -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} +} diff --git a/http/handler/context.go b/http/handler/context.go deleted file mode 100644 index 2d70271..0000000 --- a/http/handler/context.go +++ /dev/null @@ -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} -} diff --git a/http/handler/handler.go b/http/handler/handler.go deleted file mode 100644 index 137ff11..0000000 --- a/http/handler/handler.go +++ /dev/null @@ -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, - } -} diff --git a/http/handler/html_response.go b/http/handler/html_response.go deleted file mode 100644 index 4d0dbe5..0000000 --- a/http/handler/html_response.go +++ /dev/null @@ -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")) -} diff --git a/http/handler/json_response.go b/http/handler/json_response.go deleted file mode 100644 index a79268c..0000000 --- a/http/handler/json_response.go +++ /dev/null @@ -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} -} diff --git a/http/handler/request.go b/http/handler/request.go deleted file mode 100644 index 7289a70..0000000 --- a/http/handler/request.go +++ /dev/null @@ -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} -} diff --git a/http/handler/response.go b/http/handler/response.go deleted file mode 100644 index 4e4c44a..0000000 --- a/http/handler/response.go +++ /dev/null @@ -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} -} diff --git a/http/handler/xml_response.go b/http/handler/xml_response.go deleted file mode 100644 index 5b647a1..0000000 --- a/http/handler/xml_response.go +++ /dev/null @@ -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)) -} diff --git a/http/request/request.go b/http/request/request.go new file mode 100644 index 0000000..da79a3f --- /dev/null +++ b/http/request/request.go @@ -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 +} diff --git a/http/response/html/html.go b/http/response/html/html.go new file mode 100644 index 0000000..4f605e8 --- /dev/null +++ b/http/response/html/html.go @@ -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")) +} diff --git a/http/response/json/json.go b/http/response/json/json.go new file mode 100644 index 0000000..ff0f06b --- /dev/null +++ b/http/response/json/json.go @@ -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 +} diff --git a/http/response/response.go b/http/response/response.go new file mode 100644 index 0000000..066d061 --- /dev/null +++ b/http/response/response.go @@ -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) + } +} diff --git a/http/response/xml/xml.go b/http/response/xml/xml.go new file mode 100644 index 0000000..9e37e87 --- /dev/null +++ b/http/response/xml/xml.go @@ -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)) +} diff --git a/locale/translations.go b/locale/translations.go index 85d766e..33bb4f6 100755 --- a/locale/translations.go +++ b/locale/translations.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// 2018-04-09 20:38:50.319066697 -0700 PDT m=+0.026775461 +// 2018-04-29 16:22:00.540830112 -0700 PDT m=+0.025120206 package locale @@ -161,7 +161,7 @@ var translations = map[string]string{ "Scraper Rules": "Extraktionsregeln", "Rewrite Rules": "Umschreiberegeln", "Preferences saved!": "Einstellungen gespeichert!", - "Your external account is now linked !": "Ihr externes Konto wurde verlinkt!", + "Your external account is now linked!": "Ihr externes Konto wurde verlinkt!", "Save articles to Wallabag": "Artikel in Wallabag speichern", "Wallabag API Endpoint": "Wallabag URL", "Wallabag Client ID": "Wallabag Client-ID", @@ -395,7 +395,7 @@ var translations = map[string]string{ "Scraper Rules": "Règles pour récupérer le contenu original", "Rewrite Rules": "Règles de réécriture", "Preferences saved!": "Préférences sauvegardées !", - "Your external account is now linked !": "Votre compte externe est maintenant associé !", + "Your external account is now linked!": "Votre compte externe est maintenant associé !", "Save articles to Wallabag": "Sauvegarder les articles vers Wallabag", "Wallabag API Endpoint": "URL de l'API de Wallabag", "Wallabag Client ID": "Identifiant du client Wallabag", @@ -459,7 +459,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)", "Website unreachable, the request timed out after %d seconds": "Site web injoignable, la requête à échouée après %d secondes", "Comments": "Commentaires", - "View Comments": "Voir les commentaires" + "View Comments": "Voir les commentaires", + "This file is empty": "Ce fichier est vide", + "Your external account is now dissociated!": "Votre compte externe est maintenant dissocié !" } `, "nl_NL": `{ @@ -618,7 +620,7 @@ var translations = map[string]string{ "Scraper Rules": "Scraper regels", "Rewrite Rules": "Rewrite regels", "Preferences saved!": "Instellingen opgeslagen!", - "Your external account is now linked !": "Jouw externe account is nu gekoppeld!", + "Your external account is now linked!": "Jouw externe account is nu gekoppeld!", "Save articles to Wallabag": "Sauvegarder les articles vers Wallabag", "Wallabag API Endpoint": "Wallabag URL", "Wallabag Client ID": "Wallabag Client-ID", @@ -842,7 +844,7 @@ var translations = map[string]string{ "Scraper Rules": "Zasady ekstrakcji", "Rewrite Rules": "Reguły zapisu", "Preferences saved!": "Ustawienia zapisane!", - "Your external account is now linked !": "Twoje zewnętrzne konto jest teraz połączone!", + "Your external account is now linked!": "Twoje zewnętrzne konto jest teraz połączone!", "Save articles to Wallabag": "Zapisz artykuły do Wallabag", "Wallabag API Endpoint": "Wallabag URL", "Wallabag Client ID": "Wallabag Client-ID", @@ -1064,7 +1066,7 @@ var translations = map[string]string{ "Scraper Rules": "Scraper规则", "Rewrite Rules": "重写规则", "Preferences saved!": "偏好已存储!", - "Your external account is now linked !": "您的外部账号已关联!", + "Your external account is now linked!": "您的外部账号已关联!", "Save articles to Wallabag": "保存文章到Wallabag", "Wallabag API Endpoint": "Wallabag URL", "Wallabag Client ID": "Wallabag 客户端ID", @@ -1132,10 +1134,10 @@ var translations = map[string]string{ } var translationsChecksums = map[string]string{ - "de_DE": "df47fc009e6a021579c7e004ebc0b00eae7bf47c23daaf74489fb4c15881296f", + "de_DE": "791d72c96137ab03b729017bdfa27c8eed2f65912e372fcb5b2796d5099d5498", "en_US": "6fe95384260941e8a5a3c695a655a932e0a8a6a572c1e45cb2b1ae8baa01b897", - "fr_FR": "fb572aee29b90fcaa866e97d2e1cab8c6b8cd8a1cb76c7e83d7aa778748dd283", - "nl_NL": "d427d6a5e843be576040dee004df2b685a839a38b2e5f06435faa2973f1f4c70", - "pl_PL": "4dcf7c3f44c80ca81ecdbef96bdb21d1ae1a8a6caf60cc11403e5e041efc5ca9", - "zh_CN": "bfa05d3b3396df6222414a3a6949b73b486cd021499ecd3a34ce8e04e93aad93", + "fr_FR": "b4e407e3665b30b29da3bce197a035b842a9bdd7781c28cc056d978b46646f3c", + "nl_NL": "1a73f1dd1c4c0d2c2adc8695cdd050c2dad81c14876caed3892b44adc2491265", + "pl_PL": "da709c14ff71f3b516eec66cb2758d89c5feab1472c94b2b518f425162a9f806", + "zh_CN": "d80594c1b67d15e9f4673d3d62fe4949e8606a5fdfb741d8a8921f21dceb8cf2", } diff --git a/locale/translations/de_DE.json b/locale/translations/de_DE.json index 52358cb..e00ab23 100644 --- a/locale/translations/de_DE.json +++ b/locale/translations/de_DE.json @@ -155,7 +155,7 @@ "Scraper Rules": "Extraktionsregeln", "Rewrite Rules": "Umschreiberegeln", "Preferences saved!": "Einstellungen gespeichert!", - "Your external account is now linked !": "Ihr externes Konto wurde verlinkt!", + "Your external account is now linked!": "Ihr externes Konto wurde verlinkt!", "Save articles to Wallabag": "Artikel in Wallabag speichern", "Wallabag API Endpoint": "Wallabag URL", "Wallabag Client ID": "Wallabag Client-ID", diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json index 818b1ad..60ad9d8 100644 --- a/locale/translations/fr_FR.json +++ b/locale/translations/fr_FR.json @@ -155,7 +155,7 @@ "Scraper Rules": "Règles pour récupérer le contenu original", "Rewrite Rules": "Règles de réécriture", "Preferences saved!": "Préférences sauvegardées !", - "Your external account is now linked !": "Votre compte externe est maintenant associé !", + "Your external account is now linked!": "Votre compte externe est maintenant associé !", "Save articles to Wallabag": "Sauvegarder les articles vers Wallabag", "Wallabag API Endpoint": "URL de l'API de Wallabag", "Wallabag Client ID": "Identifiant du client Wallabag", @@ -219,5 +219,7 @@ "This website is permanently unreachable (original error: %q)": "Ce site web n'est pas joignable de façon permanente (erreur originale : %q)", "Website unreachable, the request timed out after %d seconds": "Site web injoignable, la requête à échouée après %d secondes", "Comments": "Commentaires", - "View Comments": "Voir les commentaires" + "View Comments": "Voir les commentaires", + "This file is empty": "Ce fichier est vide", + "Your external account is now dissociated!": "Votre compte externe est maintenant dissocié !" } diff --git a/locale/translations/nl_NL.json b/locale/translations/nl_NL.json index 01d8212..8580fad 100644 --- a/locale/translations/nl_NL.json +++ b/locale/translations/nl_NL.json @@ -154,7 +154,7 @@ "Scraper Rules": "Scraper regels", "Rewrite Rules": "Rewrite regels", "Preferences saved!": "Instellingen opgeslagen!", - "Your external account is now linked !": "Jouw externe account is nu gekoppeld!", + "Your external account is now linked!": "Jouw externe account is nu gekoppeld!", "Save articles to Wallabag": "Sauvegarder les articles vers Wallabag", "Wallabag API Endpoint": "Wallabag URL", "Wallabag Client ID": "Wallabag Client-ID", diff --git a/locale/translations/pl_PL.json b/locale/translations/pl_PL.json index 4f2bc51..2c6019d 100755 --- a/locale/translations/pl_PL.json +++ b/locale/translations/pl_PL.json @@ -157,7 +157,7 @@ "Scraper Rules": "Zasady ekstrakcji", "Rewrite Rules": "Reguły zapisu", "Preferences saved!": "Ustawienia zapisane!", - "Your external account is now linked !": "Twoje zewnętrzne konto jest teraz połączone!", + "Your external account is now linked!": "Twoje zewnętrzne konto jest teraz połączone!", "Save articles to Wallabag": "Zapisz artykuły do Wallabag", "Wallabag API Endpoint": "Wallabag URL", "Wallabag Client ID": "Wallabag Client-ID", diff --git a/locale/translations/zh_CN.json b/locale/translations/zh_CN.json index 6f12275..ccd5a6c 100644 --- a/locale/translations/zh_CN.json +++ b/locale/translations/zh_CN.json @@ -155,7 +155,7 @@ "Scraper Rules": "Scraper规则", "Rewrite Rules": "重写规则", "Preferences saved!": "偏好已存储!", - "Your external account is now linked !": "您的外部账号已关联!", + "Your external account is now linked!": "您的外部账号已关联!", "Save articles to Wallabag": "保存文章到Wallabag", "Wallabag API Endpoint": "Wallabag URL", "Wallabag Client ID": "Wallabag 客户端ID", diff --git a/middleware/app_session.go b/middleware/app_session.go index 525cd66..e898ee3 100644 --- a/middleware/app_session.go +++ b/middleware/app_session.go @@ -6,9 +6,12 @@ package middleware import ( "context" + "errors" "net/http" "github.com/miniflux/miniflux/http/cookie" + "github.com/miniflux/miniflux/http/request" + "github.com/miniflux/miniflux/http/response/html" "github.com/miniflux/miniflux/logger" "github.com/miniflux/miniflux/model" ) @@ -17,20 +20,21 @@ import ( func (m *Middleware) AppSession(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var err error - session := m.getSessionValueFromCookie(r) + session := m.getAppSessionValueFromCookie(r) if session == nil { - logger.Debug("[Middleware:Session] Session not found") + logger.Debug("[Middleware:AppSession] Session not found") + session, err = m.store.CreateSession() if err != nil { - logger.Error("[Middleware:Session] %v", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + logger.Error("[Middleware:AppSession] %v", err) + html.ServerError(w, err) return } http.SetCookie(w, cookie.New(cookie.CookieSessionID, session.ID, m.cfg.IsHTTPS, m.cfg.BasePath())) } else { - logger.Debug("[Middleware:Session] %s", session) + logger.Debug("[Middleware:AppSession] %s", session) } if r.Method == "POST" { @@ -38,9 +42,8 @@ func (m *Middleware) AppSession(next http.Handler) http.Handler { headerValue := r.Header.Get("X-Csrf-Token") if session.Data.CSRF != formValue && session.Data.CSRF != headerValue { - logger.Error(`[Middleware:Session] Invalid or missing CSRF token: Form="%s", Header="%s"`, formValue, headerValue) - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("Invalid or missing CSRF session!")) + logger.Error(`[Middleware:AppSession] Invalid or missing CSRF token: Form="%s", Header="%s"`, formValue, headerValue) + html.BadRequest(w, errors.New("invalid or missing CSRF")) return } } @@ -56,15 +59,15 @@ func (m *Middleware) AppSession(next http.Handler) http.Handler { }) } -func (m *Middleware) getSessionValueFromCookie(r *http.Request) *model.Session { - sessionCookie, err := r.Cookie(cookie.CookieSessionID) - if err == http.ErrNoCookie { +func (m *Middleware) getAppSessionValueFromCookie(r *http.Request) *model.Session { + cookieValue := request.Cookie(r, cookie.CookieSessionID) + if cookieValue == "" { return nil } - session, err := m.store.Session(sessionCookie.Value) + session, err := m.store.Session(cookieValue) if err != nil { - logger.Error("[Middleware:Session] %v", err) + logger.Error("[Middleware:AppSession] %v", err) return nil } diff --git a/middleware/basic_auth.go b/middleware/basic_auth.go index 9d7a4b2..edea333 100644 --- a/middleware/basic_auth.go +++ b/middleware/basic_auth.go @@ -8,6 +8,7 @@ import ( "context" "net/http" + "github.com/miniflux/miniflux/http/response/json" "github.com/miniflux/miniflux/logger" ) @@ -15,35 +16,30 @@ import ( func (m *Middleware) BasicAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - errorResponse := `{"error_message": "Not Authorized"}` username, password, authOK := r.BasicAuth() if !authOK { logger.Debug("[Middleware:BasicAuth] No authentication headers sent") - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte(errorResponse)) + json.Unauthorized(w) return } if err := m.store.CheckPassword(username, password); err != nil { logger.Info("[Middleware:BasicAuth] Invalid username or password: %s", username) - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte(errorResponse)) + json.Unauthorized(w) return } user, err := m.store.UserByUsername(username) if err != nil { logger.Error("[Middleware:BasicAuth] %v", err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(errorResponse)) + json.ServerError(w, err) return } if user == nil { logger.Info("[Middleware:BasicAuth] User not found: %s", username) - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte(errorResponse)) + json.Unauthorized(w) return } diff --git a/middleware/common_headers.go b/middleware/common_headers.go new file mode 100644 index 0000000..bdec580 --- /dev/null +++ b/middleware/common_headers.go @@ -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) + }) +} diff --git a/middleware/fever.go b/middleware/fever.go index c9765fe..78217e4 100644 --- a/middleware/fever.go +++ b/middleware/fever.go @@ -8,27 +8,25 @@ import ( "context" "net/http" + "github.com/miniflux/miniflux/http/response/json" "github.com/miniflux/miniflux/logger" ) // FeverAuth handles Fever API authentication. func (m *Middleware) FeverAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - logger.Debug("[Middleware:Fever]") - apiKey := r.FormValue("api_key") + user, err := m.store.UserByFeverToken(apiKey) if err != nil { - logger.Error("[Fever] %v", err) - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"api_version": 3, "auth": 0}`)) + logger.Error("[Middleware:Fever] %v", err) + json.OK(w, map[string]int{"api_version": 3, "auth": 0}) return } if user == nil { logger.Info("[Middleware:Fever] Fever authentication failure") - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"api_version": 3, "auth": 0}`)) + json.OK(w, map[string]int{"api_version": 3, "auth": 0}) return } diff --git a/middleware/logging.go b/middleware/logging.go index 6fc506a..a6c141b 100644 --- a/middleware/logging.go +++ b/middleware/logging.go @@ -8,6 +8,7 @@ import ( "net/http" "github.com/miniflux/miniflux/logger" + "github.com/tomasen/realip" ) diff --git a/middleware/user_session.go b/middleware/user_session.go index 2cb9f8a..b27858f 100644 --- a/middleware/user_session.go +++ b/middleware/user_session.go @@ -9,6 +9,8 @@ import ( "net/http" "github.com/miniflux/miniflux/http/cookie" + "github.com/miniflux/miniflux/http/request" + "github.com/miniflux/miniflux/http/response" "github.com/miniflux/miniflux/http/route" "github.com/miniflux/miniflux/logger" "github.com/miniflux/miniflux/model" @@ -19,17 +21,18 @@ import ( // UserSession handles the user session middleware. func (m *Middleware) UserSession(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := m.getSessionFromCookie(r) + session := m.getUserSessionFromCookie(r) if session == nil { logger.Debug("[Middleware:UserSession] Session not found") if m.isPublicRoute(r) { next.ServeHTTP(w, r) } else { - http.Redirect(w, r, route.Path(m.router, "login"), http.StatusFound) + response.Redirect(w, r, route.Path(m.router, "login")) } } else { logger.Debug("[Middleware:UserSession] %s", session) + ctx := r.Context() ctx = context.WithValue(ctx, UserIDContextKey, session.UserID) ctx = context.WithValue(ctx, IsAuthenticatedContextKey, true) @@ -58,13 +61,13 @@ func (m *Middleware) isPublicRoute(r *http.Request) bool { } } -func (m *Middleware) getSessionFromCookie(r *http.Request) *model.UserSession { - sessionCookie, err := r.Cookie(cookie.CookieUserSessionID) - if err == http.ErrNoCookie { +func (m *Middleware) getUserSessionFromCookie(r *http.Request) *model.UserSession { + cookieValue := request.Cookie(r, cookie.CookieUserSessionID) + if cookieValue == "" { return nil } - session, err := m.store.UserSessionByToken(sessionCookie.Value) + session, err := m.store.UserSessionByToken(cookieValue) if err != nil { logger.Error("[Middleware:UserSession] %v", err) return nil diff --git a/storage/category.go b/storage/category.go index 197b78b..bbb4c86 100644 --- a/storage/category.go +++ b/storage/category.go @@ -112,7 +112,8 @@ func (s *Storage) CategoriesWithFeedCount(userID int64) (model.Categories, error query := `SELECT c.id, c.user_id, c.title, (SELECT count(*) FROM feeds WHERE feeds.category_id=c.id) AS count - FROM categories c WHERE user_id=$1` + FROM categories c WHERE user_id=$1 + ORDER BY c.title ASC` rows, err := s.db.Query(query, userID) if err != nil { diff --git a/storage/entry.go b/storage/entry.go index d56d5de..fb1fb59 100644 --- a/storage/entry.go +++ b/storage/entry.go @@ -16,6 +16,20 @@ import ( "github.com/lib/pq" ) +// CountUnreadEntries returns the number of unread entries. +func (s *Storage) CountUnreadEntries(userID int64) int { + builder := s.NewEntryQueryBuilder(userID) + builder.WithStatus(model.EntryStatusUnread) + + n, err := builder.CountEntries() + if err != nil { + logger.Error("unable to count unread entries: %v", err) + return 0 + } + + return n +} + // NewEntryQueryBuilder returns a new EntryQueryBuilder func (s *Storage) NewEntryQueryBuilder(userID int64) *EntryQueryBuilder { return NewEntryQueryBuilder(s, userID) diff --git a/storage/user_session.go b/storage/user_session.go index e9e8ebe..990157d 100644 --- a/storage/user_session.go +++ b/storage/user_session.go @@ -47,24 +47,20 @@ func (s *Storage) UserSessions(userID int64) (model.UserSessions, error) { } // CreateUserSession creates a new sessions. -func (s *Storage) CreateUserSession(username, userAgent, ip string) (sessionID string, err error) { - var userID int64 - +func (s *Storage) CreateUserSession(username, userAgent, ip string) (sessionID string, userID int64, err error) { err = s.db.QueryRow("SELECT id FROM users WHERE username = LOWER($1)", username).Scan(&userID) if err != nil { - return "", fmt.Errorf("unable to fetch UserID: %v", err) + return "", 0, fmt.Errorf("unable to fetch user ID: %v", err) } token := crypto.GenerateRandomString(64) query := "INSERT INTO user_sessions (token, user_id, user_agent, ip) VALUES ($1, $2, $3, $4)" _, err = s.db.Exec(query, token, userID, userAgent, ip) if err != nil { - return "", fmt.Errorf("unable to create user session: %v", err) + return "", 0, fmt.Errorf("unable to create user session: %v", err) } - s.SetLastLogin(userID) - - return token, nil + return token, userID, nil } // UserSessionByToken finds a session by the token. diff --git a/template/common.go b/template/common.go index f3fd5ee..62645ef 100644 --- a/template/common.go +++ b/template/common.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// 2018-04-07 13:51:33.926223471 -0700 PDT m=+0.022666283 +// 2018-04-29 16:22:00.539326448 -0700 PDT m=+0.023616542 package template diff --git a/template/engine.go b/template/engine.go index 1afeaf9..8055582 100644 --- a/template/engine.go +++ b/template/engine.go @@ -7,7 +7,6 @@ package template import ( "bytes" "html/template" - "io" "time" "github.com/miniflux/miniflux/config" @@ -38,7 +37,7 @@ func (e *Engine) parseAll() { } // 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] if !ok { 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) } - b.WriteTo(w) + return b.Bytes() } // NewEngine returns a new template engine. diff --git a/template/views.go b/template/views.go index 6af93ee..712dc60 100644 --- a/template/views.go +++ b/template/views.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// 2018-04-07 13:51:33.918407222 -0700 PDT m=+0.014850034 +// 2018-04-29 16:22:00.531039167 -0700 PDT m=+0.015329261 package template diff --git a/ui/about.go b/ui/about.go index 4de46a8..7618e01 100644 --- a/ui/about.go +++ b/ui/about.go @@ -5,21 +5,32 @@ package ui import ( - "github.com/miniflux/miniflux/http/handler" + "net/http" + + "github.com/miniflux/miniflux/http/context" + "github.com/miniflux/miniflux/http/response/html" + "github.com/miniflux/miniflux/ui/session" + "github.com/miniflux/miniflux/ui/view" "github.com/miniflux/miniflux/version" ) -// AboutPage shows the about page. -func (c *Controller) AboutPage(ctx *handler.Context, request *handler.Request, response *handler.Response) { - args, err := c.getCommonTemplateArgs(ctx) +// About shows the about page. +func (c *Controller) About(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) + + user, err := c.store.UserByID(ctx.UserID()) if err != nil { - response.HTML().ServerError(err) + html.ServerError(w, err) return } - response.HTML().Render("about", ctx.UserLanguage(), args.Merge(tplParams{ - "version": version.Version, - "build_date": version.BuildDate, - "menu": "settings", - })) + sess := session.New(c.store, ctx) + view := view.New(c.tpl, ctx, sess) + view.Set("version", version.Version) + view.Set("build_date", version.BuildDate) + view.Set("menu", "settings") + view.Set("user", user) + view.Set("countUnread", c.store.CountUnreadEntries(user.ID)) + + html.OK(w, view.Render("about")) } diff --git a/ui/bookmark_entries.go b/ui/bookmark_entries.go new file mode 100644 index 0000000..c5f7de3 --- /dev/null +++ b/ui/bookmark_entries.go @@ -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")) +} diff --git a/ui/category.go b/ui/category.go deleted file mode 100644 index 9ffe9a8..0000000 --- a/ui/category.go +++ /dev/null @@ -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 -} diff --git a/ui/category_create.go b/ui/category_create.go new file mode 100644 index 0000000..6060620 --- /dev/null +++ b/ui/category_create.go @@ -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")) +} diff --git a/ui/category_edit.go b/ui/category_edit.go new file mode 100644 index 0000000..df8f619 --- /dev/null +++ b/ui/category_edit.go @@ -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")) +} diff --git a/ui/category_entries.go b/ui/category_entries.go new file mode 100644 index 0000000..309ddb3 --- /dev/null +++ b/ui/category_entries.go @@ -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")) +} diff --git a/ui/category_list.go b/ui/category_list.go new file mode 100644 index 0000000..c315d77 --- /dev/null +++ b/ui/category_list.go @@ -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")) +} diff --git a/ui/category_remove.go b/ui/category_remove.go new file mode 100644 index 0000000..6656ee3 --- /dev/null +++ b/ui/category_remove.go @@ -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")) +} diff --git a/ui/category_save.go b/ui/category_save.go new file mode 100644 index 0000000..d186b28 --- /dev/null +++ b/ui/category_save.go @@ -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")) +} diff --git a/ui/category_update.go b/ui/category_update.go new file mode 100644 index 0000000..daa6a02 --- /dev/null +++ b/ui/category_update.go @@ -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")) +} diff --git a/ui/controller.go b/ui/controller.go index a7af48b..b3e85af 100644 --- a/ui/controller.go +++ b/ui/controller.go @@ -5,59 +5,35 @@ package ui import ( + "github.com/gorilla/mux" "github.com/miniflux/miniflux/config" - "github.com/miniflux/miniflux/http/handler" - "github.com/miniflux/miniflux/model" + "github.com/miniflux/miniflux/locale" "github.com/miniflux/miniflux/reader/feed" "github.com/miniflux/miniflux/scheduler" "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. type Controller struct { cfg *config.Config store *storage.Storage pool *scheduler.WorkerPool feedHandler *feed.Handler -} - -func (c *Controller) getCommonTemplateArgs(ctx *handler.Context) (tplParams, error) { - 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 + tpl *template.Engine + router *mux.Router + translator *locale.Translator } // 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{ cfg: cfg, store: store, pool: pool, feedHandler: feedHandler, + tpl: tpl, + translator: translator, + router: router, } } diff --git a/ui/entry.go b/ui/entry.go deleted file mode 100644 index 436f189..0000000 --- a/ui/entry.go +++ /dev/null @@ -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 -} diff --git a/ui/entry_category.go b/ui/entry_category.go new file mode 100644 index 0000000..a8766bb --- /dev/null +++ b/ui/entry_category.go @@ -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")) +} diff --git a/ui/entry_feed.go b/ui/entry_feed.go new file mode 100644 index 0000000..dfa85bc --- /dev/null +++ b/ui/entry_feed.go @@ -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")) +} diff --git a/ui/entry_prev_next.go b/ui/entry_prev_next.go new file mode 100644 index 0000000..9ab415e --- /dev/null +++ b/ui/entry_prev_next.go @@ -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 +} diff --git a/ui/entry_read.go b/ui/entry_read.go new file mode 100644 index 0000000..40c5074 --- /dev/null +++ b/ui/entry_read.go @@ -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")) +} diff --git a/ui/entry_save.go b/ui/entry_save.go new file mode 100644 index 0000000..9b24078 --- /dev/null +++ b/ui/entry_save.go @@ -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"}) +} diff --git a/ui/entry_scraper.go b/ui/entry_scraper.go new file mode 100644 index 0000000..75e4040 --- /dev/null +++ b/ui/entry_scraper.go @@ -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}) +} diff --git a/ui/entry_starred.go b/ui/entry_starred.go new file mode 100644 index 0000000..66ca85d --- /dev/null +++ b/ui/entry_starred.go @@ -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")) +} diff --git a/ui/entry_toggle_bookmark.go b/ui/entry_toggle_bookmark.go new file mode 100644 index 0000000..423fde8 --- /dev/null +++ b/ui/entry_toggle_bookmark.go @@ -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") +} diff --git a/ui/entry_unread.go b/ui/entry_unread.go new file mode 100644 index 0000000..d103e6d --- /dev/null +++ b/ui/entry_unread.go @@ -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")) +} diff --git a/ui/entry_update_status.go b/ui/entry_update_status.go new file mode 100644 index 0000000..3174d85 --- /dev/null +++ b/ui/entry_update_status.go @@ -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") +} diff --git a/ui/feed.go b/ui/feed.go deleted file mode 100644 index ae0a54e..0000000 --- a/ui/feed.go +++ /dev/null @@ -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 -} diff --git a/ui/feed_edit.go b/ui/feed_edit.go new file mode 100644 index 0000000..d6062c8 --- /dev/null +++ b/ui/feed_edit.go @@ -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")) +} diff --git a/ui/feed_entries.go b/ui/feed_entries.go new file mode 100644 index 0000000..0143562 --- /dev/null +++ b/ui/feed_entries.go @@ -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")) +} diff --git a/ui/feed_icon.go b/ui/feed_icon.go new file mode 100644 index 0000000..c05efe5 --- /dev/null +++ b/ui/feed_icon.go @@ -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) +} diff --git a/ui/feed_list.go b/ui/feed_list.go new file mode 100644 index 0000000..fcf315c --- /dev/null +++ b/ui/feed_list.go @@ -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")) +} diff --git a/ui/feed_refresh.go b/ui/feed_refresh.go new file mode 100644 index 0000000..eb86b04 --- /dev/null +++ b/ui/feed_refresh.go @@ -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")) +} diff --git a/ui/feed_remove.go b/ui/feed_remove.go new file mode 100644 index 0000000..23d877b --- /dev/null +++ b/ui/feed_remove.go @@ -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")) +} diff --git a/ui/feed_update.go b/ui/feed_update.go new file mode 100644 index 0000000..8157327 --- /dev/null +++ b/ui/feed_update.go @@ -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)) +} diff --git a/ui/history.go b/ui/history.go deleted file mode 100644 index f9c8ab5..0000000 --- a/ui/history.go +++ /dev/null @@ -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")) -} diff --git a/ui/history_entries.go b/ui/history_entries.go new file mode 100644 index 0000000..e695ce3 --- /dev/null +++ b/ui/history_entries.go @@ -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")) +} diff --git a/ui/history_flush.go b/ui/history_flush.go new file mode 100644 index 0000000..96b6f3d --- /dev/null +++ b/ui/history_flush.go @@ -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")) +} diff --git a/ui/icon.go b/ui/icon.go deleted file mode 100644 index 4c445f0..0000000 --- a/ui/icon.go +++ /dev/null @@ -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) -} diff --git a/ui/integration_show.go b/ui/integration_show.go new file mode 100644 index 0000000..703f610 --- /dev/null +++ b/ui/integration_show.go @@ -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")) +} diff --git a/ui/integration_update.go b/ui/integration_update.go new file mode 100644 index 0000000..a1e98cb --- /dev/null +++ b/ui/integration_update.go @@ -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")) +} diff --git a/ui/integrations.go b/ui/integrations.go deleted file mode 100644 index a980f9b..0000000 --- a/ui/integrations.go +++ /dev/null @@ -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")) -} diff --git a/ui/login.go b/ui/login.go deleted file mode 100644 index 18571d8..0000000 --- a/ui/login.go +++ /dev/null @@ -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")) -} diff --git a/ui/login_check.go b/ui/login_check.go new file mode 100644 index 0000000..bc49c8d --- /dev/null +++ b/ui/login_check.go @@ -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")) +} diff --git a/ui/login_show.go b/ui/login_show.go new file mode 100644 index 0000000..84dc160 --- /dev/null +++ b/ui/login_show.go @@ -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")) +} diff --git a/ui/logout.go b/ui/logout.go new file mode 100644 index 0000000..2946d1a --- /dev/null +++ b/ui/logout.go @@ -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")) +} diff --git a/ui/oauth2.go b/ui/oauth2.go index c5bf931..f1e6a4e 100644 --- a/ui/oauth2.go +++ b/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 // license that can be found in the LICENSE file. @@ -6,162 +6,9 @@ package ui import ( "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/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 { return oauth2.NewManager( cfg.OAuth2ClientID(), diff --git a/ui/oauth2_callback.go b/ui/oauth2_callback.go new file mode 100644 index 0000000..f564c7e --- /dev/null +++ b/ui/oauth2_callback.go @@ -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")) +} diff --git a/ui/oauth2_redirect.go b/ui/oauth2_redirect.go new file mode 100644 index 0000000..7f472ac --- /dev/null +++ b/ui/oauth2_redirect.go @@ -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())) +} diff --git a/ui/oauth2_unlink.go b/ui/oauth2_unlink.go new file mode 100644 index 0000000..e67c6fe --- /dev/null +++ b/ui/oauth2_unlink.go @@ -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 +} diff --git a/ui/opml.go b/ui/opml.go deleted file mode 100644 index 3ca68ff..0000000 --- a/ui/opml.go +++ /dev/null @@ -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")) -} diff --git a/ui/opml_export.go b/ui/opml_export.go new file mode 100644 index 0000000..ec396f0 --- /dev/null +++ b/ui/opml_export.go @@ -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) +} diff --git a/ui/opml_import.go b/ui/opml_import.go new file mode 100644 index 0000000..fd3a869 --- /dev/null +++ b/ui/opml_import.go @@ -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")) +} diff --git a/ui/opml_upload.go b/ui/opml_upload.go new file mode 100644 index 0000000..ef93fb1 --- /dev/null +++ b/ui/opml_upload.go @@ -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")) +} diff --git a/ui/payload.go b/ui/payload.go index 2841828..bd65c83 100644 --- a/ui/payload.go +++ b/ui/payload.go @@ -12,14 +12,15 @@ import ( "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 { EntryIDs []int64 `json:"entry_ids"` Status string `json:"status"` } var p payload - decoder := json.NewDecoder(data) + decoder := json.NewDecoder(r) + defer r.Close() if err = decoder.Decode(&p); err != nil { return nil, "", fmt.Errorf("invalid JSON payload: %v", err) } diff --git a/ui/proxy.go b/ui/proxy.go index 78ab67d..62364aa 100644 --- a/ui/proxy.go +++ b/ui/proxy.go @@ -8,31 +8,34 @@ import ( "encoding/base64" "errors" "io/ioutil" + "net/http" "time" "github.com/miniflux/miniflux/crypto" "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" ) // 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 request.Request().Header.Get("If-None-Match") != "" { - response.NotModified() + if r.Header.Get("If-None-Match") != "" { + response.NotModified(w) return } - encodedURL := request.StringParam("encodedURL", "") + encodedURL := request.Param(r, "encodedURL", "") if encodedURL == "" { - response.HTML().BadRequest(errors.New("No URL provided")) + html.BadRequest(w, errors.New("No URL provided")) return } decodedURL, err := base64.URLEncoding.DecodeString(encodedURL) if err != nil { - response.HTML().BadRequest(errors.New("Unable to decode this URL")) + html.BadRequest(w, errors.New("Unable to decode this URL")) return } @@ -40,17 +43,17 @@ func (c *Controller) ImageProxy(ctx *handler.Context, request *handler.Request, resp, err := clt.Get() if err != nil { logger.Error("[Controller:ImageProxy] %v", err) - response.HTML().NotFound() + html.NotFound(w) return } if resp.HasServerFailure() { - response.HTML().NotFound() + html.NotFound(w) return } body, _ := ioutil.ReadAll(resp.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) } diff --git a/ui/session.go b/ui/session.go deleted file mode 100644 index 49f81d1..0000000 --- a/ui/session.go +++ /dev/null @@ -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")) -} diff --git a/ui/session/session.go b/ui/session/session.go new file mode 100644 index 0000000..fc25406 --- /dev/null +++ b/ui/session/session.go @@ -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} +} diff --git a/ui/session_list.go b/ui/session_list.go new file mode 100644 index 0000000..cd9bfe8 --- /dev/null +++ b/ui/session_list.go @@ -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")) +} diff --git a/ui/session_remove.go b/ui/session_remove.go new file mode 100644 index 0000000..1ee2a6b --- /dev/null +++ b/ui/session_remove.go @@ -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")) +} diff --git a/ui/settings.go b/ui/settings.go deleted file mode 100644 index 88dc155..0000000 --- a/ui/settings.go +++ /dev/null @@ -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 -} diff --git a/ui/settings_show.go b/ui/settings_show.go new file mode 100644 index 0000000..c23473b --- /dev/null +++ b/ui/settings_show.go @@ -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")) +} diff --git a/ui/settings_update.go b/ui/settings_update.go new file mode 100644 index 0000000..a8b49ff --- /dev/null +++ b/ui/settings_update.go @@ -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")) +} diff --git a/ui/starred.go b/ui/starred.go deleted file mode 100644 index 3ebd359..0000000 --- a/ui/starred.go +++ /dev/null @@ -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") -} diff --git a/ui/static.go b/ui/static.go deleted file mode 100644 index 266d9e6..0000000 --- a/ui/static.go +++ /dev/null @@ -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) -} diff --git a/ui/static_app_icon.go b/ui/static_app_icon.go new file mode 100644 index 0000000..19a823e --- /dev/null +++ b/ui/static_app_icon.go @@ -0,0 +1,37 @@ +// 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 ( + "encoding/base64" + "net/http" + "time" + + "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/ui/static" +) + +// AppIcon renders application icons. +func (c *Controller) AppIcon(w http.ResponseWriter, r *http.Request) { + filename := request.Param(r, "filename", "favicon.png") + encodedBlob, found := static.Binaries[filename] + if !found { + logger.Info("[Controller:AppIcon] This icon doesn't exists: %s", filename) + html.NotFound(w) + return + } + + blob, err := base64.StdEncoding.DecodeString(encodedBlob) + if err != nil { + logger.Error("[Controller:AppIcon] %v", err) + html.NotFound(w) + return + } + + response.Cache(w, r, "image/png", static.BinariesChecksums[filename], blob, 48*time.Hour) +} diff --git a/ui/static_favicon.go b/ui/static_favicon.go new file mode 100644 index 0000000..c68c556 --- /dev/null +++ b/ui/static_favicon.go @@ -0,0 +1,28 @@ +// 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 ( + "encoding/base64" + "net/http" + "time" + + "github.com/miniflux/miniflux/http/response" + "github.com/miniflux/miniflux/http/response/html" + "github.com/miniflux/miniflux/logger" + "github.com/miniflux/miniflux/ui/static" +) + +// Favicon renders the application favicon. +func (c *Controller) Favicon(w http.ResponseWriter, r *http.Request) { + blob, err := base64.StdEncoding.DecodeString(static.Binaries["favicon.ico"]) + if err != nil { + logger.Error("[Controller:Favicon] %v", err) + html.NotFound(w) + return + } + + response.Cache(w, r, "image/x-icon", static.BinariesChecksums["favicon.ico"], blob, 48*time.Hour) +} diff --git a/ui/static_javascript.go b/ui/static_javascript.go new file mode 100644 index 0000000..821a339 --- /dev/null +++ b/ui/static_javascript.go @@ -0,0 +1,18 @@ +// 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" + "time" + + "github.com/miniflux/miniflux/http/response" + "github.com/miniflux/miniflux/ui/static" +) + +// Javascript renders application client side code. +func (c *Controller) Javascript(w http.ResponseWriter, r *http.Request) { + response.Cache(w, r, "text/javascript; charset=utf-8", static.JavascriptChecksums["app"], []byte(static.Javascript["app"]), 48*time.Hour) +} diff --git a/ui/static_manifest.go b/ui/static_manifest.go new file mode 100644 index 0000000..8721718 --- /dev/null +++ b/ui/static_manifest.go @@ -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/http/response/json" + "github.com/miniflux/miniflux/http/route" +) + +// WebManifest renders web manifest file. +func (c *Controller) WebManifest(w http.ResponseWriter, r *http.Request) { + 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: route.Path(c.router, "unread"), + Icons: []webManifestIcon{ + webManifestIcon{Source: route.Path(c.router, "appIcon", "filename", "touch-icon-ipad-retina.png"), Sizes: "144x144", Type: "image/png"}, + webManifestIcon{Source: route.Path(c.router, "appIcon", "filename", "touch-icon-iphone-retina.png"), Sizes: "114x114", Type: "image/png"}, + }, + } + + json.OK(w, manifest) +} diff --git a/ui/static_stylesheet.go b/ui/static_stylesheet.go new file mode 100644 index 0000000..2c58ba5 --- /dev/null +++ b/ui/static_stylesheet.go @@ -0,0 +1,28 @@ +// 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" + "time" + + "github.com/miniflux/miniflux/http/request" + "github.com/miniflux/miniflux/http/response" + "github.com/miniflux/miniflux/ui/static" +) + +// Stylesheet renders the CSS. +func (c *Controller) Stylesheet(w http.ResponseWriter, r *http.Request) { + stylesheet := request.Param(r, "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(w, r, "text/css; charset=utf-8", etag, []byte(body), 48*time.Hour) +} diff --git a/ui/subscription.go b/ui/subscription.go deleted file mode 100644 index e31ab60..0000000 --- a/ui/subscription.go +++ /dev/null @@ -1,145 +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" - "github.com/miniflux/miniflux/reader/subscription" - "github.com/miniflux/miniflux/ui/form" -) - -// Bookmarklet prefill the form to add a subscription from the URL provided by the bookmarklet. -func (c *Controller) Bookmarklet(ctx *handler.Context, request *handler.Request, response *handler.Response) { - user := ctx.LoggedUser() - args, err := c.getSubscriptionFormTemplateArgs(ctx, user) - if err != nil { - response.HTML().ServerError(err) - return - } - - bookmarkletURL := request.QueryStringParam("uri", "") - response.HTML().Render("add_subscription", ctx.UserLanguage(), args.Merge(tplParams{ - "form": &form.SubscriptionForm{URL: bookmarkletURL}, - })) -} - -// AddSubscription shows the form to add a new feed. -func (c *Controller) AddSubscription(ctx *handler.Context, request *handler.Request, response *handler.Response) { - user := ctx.LoggedUser() - - args, err := c.getSubscriptionFormTemplateArgs(ctx, user) - if err != nil { - response.HTML().ServerError(err) - return - } - - response.HTML().Render("add_subscription", ctx.UserLanguage(), args) -} - -// SubmitSubscription try to find a feed from the URL provided by the user. -func (c *Controller) SubmitSubscription(ctx *handler.Context, request *handler.Request, response *handler.Response) { - user := ctx.LoggedUser() - - args, err := c.getSubscriptionFormTemplateArgs(ctx, user) - if err != nil { - response.HTML().ServerError(err) - return - } - - subscriptionForm := form.NewSubscriptionForm(request.Request()) - if err := subscriptionForm.Validate(); err != nil { - response.HTML().Render("add_subscription", ctx.UserLanguage(), args.Merge(tplParams{ - "form": subscriptionForm, - "errorMessage": err.Error(), - })) - return - } - - subscriptions, err := subscription.FindSubscriptions(subscriptionForm.URL) - if err != nil { - logger.Error("[Controller:SubmitSubscription] %v", err) - response.HTML().Render("add_subscription", ctx.UserLanguage(), args.Merge(tplParams{ - "form": subscriptionForm, - "errorMessage": err, - })) - return - } - - logger.Debug("[UI:SubmitSubscription] %s", subscriptions) - - n := len(subscriptions) - switch { - case n == 0: - response.HTML().Render("add_subscription", ctx.UserLanguage(), args.Merge(tplParams{ - "form": subscriptionForm, - "errorMessage": "Unable to find any subscription.", - })) - case n == 1: - feed, err := c.feedHandler.CreateFeed(user.ID, subscriptionForm.CategoryID, subscriptions[0].URL, subscriptionForm.Crawler) - if err != nil { - response.HTML().Render("add_subscription", ctx.UserLanguage(), args.Merge(tplParams{ - "form": subscriptionForm, - "errorMessage": err, - })) - return - } - - response.Redirect(ctx.Route("feedEntries", "feedID", feed.ID)) - case n > 1: - response.HTML().Render("choose_subscription", ctx.UserLanguage(), args.Merge(tplParams{ - "categoryID": subscriptionForm.CategoryID, - "subscriptions": subscriptions, - })) - } -} - -// ChooseSubscription shows a page to choose a subscription. -func (c *Controller) ChooseSubscription(ctx *handler.Context, request *handler.Request, response *handler.Response) { - user := ctx.LoggedUser() - - args, err := c.getSubscriptionFormTemplateArgs(ctx, user) - if err != nil { - response.HTML().ServerError(err) - return - } - - subscriptionForm := form.NewSubscriptionForm(request.Request()) - if err := subscriptionForm.Validate(); err != nil { - response.HTML().Render("add_subscription", ctx.UserLanguage(), args.Merge(tplParams{ - "form": subscriptionForm, - "errorMessage": err.Error(), - })) - return - } - - feed, err := c.feedHandler.CreateFeed(user.ID, subscriptionForm.CategoryID, subscriptionForm.URL, subscriptionForm.Crawler) - if err != nil { - response.HTML().Render("add_subscription", ctx.UserLanguage(), args.Merge(tplParams{ - "form": subscriptionForm, - "errorMessage": err, - })) - return - } - - response.Redirect(ctx.Route("feedEntries", "feedID", feed.ID)) -} - -func (c *Controller) getSubscriptionFormTemplateArgs(ctx *handler.Context, user *model.User) (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 - } - - args["categories"] = categories - args["menu"] = "feeds" - return args, nil -} diff --git a/ui/subscription_add.go b/ui/subscription_add.go new file mode 100644 index 0000000..301c5c9 --- /dev/null +++ b/ui/subscription_add.go @@ -0,0 +1,40 @@ +// 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" +) + +// AddSubscription shows the form to add a new feed. +func (c *Controller) AddSubscription(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 + } + + categories, err := c.store.Categories(user.ID) + if err != nil { + html.ServerError(w, err) + return + } + + view.Set("categories", categories) + view.Set("menu", "feeds") + view.Set("user", user) + view.Set("countUnread", c.store.CountUnreadEntries(user.ID)) + + html.OK(w, view.Render("add_subscription")) +} diff --git a/ui/subscription_bookmarklet.go b/ui/subscription_bookmarklet.go new file mode 100644 index 0000000..4f31fa8 --- /dev/null +++ b/ui/subscription_bookmarklet.go @@ -0,0 +1,45 @@ +// 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" +) + +// Bookmarklet prefill the form to add a subscription from the URL provided by the bookmarklet. +func (c *Controller) Bookmarklet(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 + } + + categories, err := c.store.Categories(user.ID) + if err != nil { + html.ServerError(w, err) + return + } + + bookmarkletURL := request.QueryParam(r, "uri", "") + + view.Set("form", form.SubscriptionForm{URL: bookmarkletURL}) + view.Set("categories", categories) + view.Set("menu", "feeds") + view.Set("user", user) + view.Set("countUnread", c.store.CountUnreadEntries(user.ID)) + + html.OK(w, view.Render("add_subscription")) +} diff --git a/ui/subscription_choose.go b/ui/subscription_choose.go new file mode 100644 index 0000000..be97441 --- /dev/null +++ b/ui/subscription_choose.go @@ -0,0 +1,59 @@ +// 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/ui/form" + "github.com/miniflux/miniflux/ui/session" + "github.com/miniflux/miniflux/ui/view" +) + +// ChooseSubscription shows a page to choose a subscription. +func (c *Controller) ChooseSubscription(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 + } + + categories, err := c.store.Categories(user.ID) + if err != nil { + html.ServerError(w, err) + return + } + + view.Set("categories", categories) + view.Set("menu", "feeds") + view.Set("user", user) + view.Set("countUnread", c.store.CountUnreadEntries(user.ID)) + + subscriptionForm := form.NewSubscriptionForm(r) + if err := subscriptionForm.Validate(); err != nil { + view.Set("form", subscriptionForm) + view.Set("errorMessage", err.Error()) + html.OK(w, view.Render("add_subscription")) + return + } + + feed, err := c.feedHandler.CreateFeed(user.ID, subscriptionForm.CategoryID, subscriptionForm.URL, subscriptionForm.Crawler) + if err != nil { + view.Set("form", subscriptionForm) + view.Set("errorMessage", err) + html.OK(w, view.Render("add_subscription")) + return + } + + response.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID)) +} diff --git a/ui/subscription_submit.go b/ui/subscription_submit.go new file mode 100644 index 0000000..f4767a2 --- /dev/null +++ b/ui/subscription_submit.go @@ -0,0 +1,89 @@ +// 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/subscription" + "github.com/miniflux/miniflux/ui/form" + "github.com/miniflux/miniflux/ui/session" + "github.com/miniflux/miniflux/ui/view" +) + +// SubmitSubscription try to find a feed from the URL provided by the user. +func (c *Controller) SubmitSubscription(w http.ResponseWriter, r *http.Request) { + ctx := context.New(r) + sess := session.New(c.store, ctx) + v := view.New(c.tpl, ctx, sess) + + user, err := c.store.UserByID(ctx.UserID()) + if err != nil { + html.ServerError(w, err) + return + } + + categories, err := c.store.Categories(user.ID) + if err != nil { + html.ServerError(w, err) + return + } + + v.Set("categories", categories) + v.Set("menu", "feeds") + v.Set("user", user) + v.Set("countUnread", c.store.CountUnreadEntries(user.ID)) + + subscriptionForm := form.NewSubscriptionForm(r) + if err := subscriptionForm.Validate(); err != nil { + v.Set("form", subscriptionForm) + v.Set("errorMessage", err.Error()) + html.OK(w, v.Render("add_subscription")) + return + } + + subscriptions, err := subscription.FindSubscriptions(subscriptionForm.URL) + if err != nil { + logger.Error("[Controller:SubmitSubscription] %v", err) + v.Set("form", subscriptionForm) + v.Set("errorMessage", err) + html.OK(w, v.Render("add_subscription")) + return + } + + logger.Debug("[UI:SubmitSubscription] %s", subscriptions) + + n := len(subscriptions) + switch { + case n == 0: + v.Set("form", subscriptionForm) + v.Set("errorMessage", "Unable to find any subscription.") + html.OK(w, v.Render("add_subscription")) + case n == 1: + feed, err := c.feedHandler.CreateFeed(user.ID, subscriptionForm.CategoryID, subscriptions[0].URL, subscriptionForm.Crawler) + if err != nil { + v.Set("form", subscriptionForm) + v.Set("errorMessage", err) + html.OK(w, v.Render("add_subscription")) + return + } + + response.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID)) + case n > 1: + v := view.New(c.tpl, ctx, sess) + v.Set("subscriptions", subscriptions) + v.Set("categoryID", subscriptionForm.CategoryID) + v.Set("menu", "feeds") + v.Set("user", user) + v.Set("countUnread", c.store.CountUnreadEntries(user.ID)) + + html.OK(w, v.Render("choose_subscription")) + } +} diff --git a/ui/unread.go b/ui/unread.go deleted file mode 100644 index f38a12f..0000000 --- a/ui/unread.go +++ /dev/null @@ -1,59 +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" -) - -// ShowUnreadPage render the page with all unread entries. -func (c *Controller) ShowUnreadPage(ctx *handler.Context, request *handler.Request, response *handler.Response) { - user := ctx.LoggedUser() - offset := request.QueryIntegerParam("offset", 0) - - builder := c.store.NewEntryQueryBuilder(user.ID) - builder.WithStatus(model.EntryStatusUnread) - countUnread, err := builder.CountEntries() - if err != nil { - response.HTML().ServerError(err) - return - } - - if offset >= countUnread { - offset = 0 - } - - builder = c.store.NewEntryQueryBuilder(user.ID) - builder.WithStatus(model.EntryStatusUnread) - 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 - } - - response.HTML().Render("unread", ctx.UserLanguage(), tplParams{ - "user": user, - "countUnread": countUnread, - "entries": entries, - "pagination": c.getPagination(ctx.Route("unread"), countUnread, offset), - "menu": "unread", - "csrf": ctx.CSRF(), - }) -} - -// MarkAllAsRead marks all unread entries as read. -func (c *Controller) MarkAllAsRead(ctx *handler.Context, request *handler.Request, response *handler.Response) { - if err := c.store.MarkAllAsRead(ctx.UserID()); err != nil { - logger.Error("[MarkAllAsRead] %v", err) - } - - response.Redirect(ctx.Route("unread")) -} diff --git a/ui/unread_entries.go b/ui/unread_entries.go new file mode 100644 index 0000000..2ae47a7 --- /dev/null +++ b/ui/unread_entries.go @@ -0,0 +1,63 @@ +// 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" +) + +// ShowUnreadPage render the page with all unread entries. +func (c *Controller) ShowUnreadPage(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 + } + + offset := request.QueryIntParam(r, "offset", 0) + builder := c.store.NewEntryQueryBuilder(user.ID) + builder.WithStatus(model.EntryStatusUnread) + countUnread, err := builder.CountEntries() + if err != nil { + html.ServerError(w, err) + return + } + + if offset >= countUnread { + offset = 0 + } + + builder = c.store.NewEntryQueryBuilder(user.ID) + builder.WithStatus(model.EntryStatusUnread) + 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 + } + + view.Set("entries", entries) + view.Set("pagination", c.getPagination(route.Path(c.router, "unread"), countUnread, offset)) + view.Set("menu", "unread") + view.Set("user", user) + view.Set("countUnread", c.store.CountUnreadEntries(user.ID)) + + html.OK(w, view.Render("unread")) +} diff --git a/ui/unread_mark_all_read.go b/ui/unread_mark_all_read.go new file mode 100644 index 0000000..2c745e4 --- /dev/null +++ b/ui/unread_mark_all_read.go @@ -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 ui + +import ( + "net/http" + + "github.com/miniflux/miniflux/http/context" + "github.com/miniflux/miniflux/http/response" + "github.com/miniflux/miniflux/http/route" + "github.com/miniflux/miniflux/logger" +) + +// MarkAllAsRead marks all unread entries as read. +func (c *Controller) MarkAllAsRead(w http.ResponseWriter, r *http.Request) { + if err := c.store.MarkAllAsRead(context.New(r).UserID()); err != nil { + logger.Error("[MarkAllAsRead] %v", err) + } + + response.Redirect(w, r, route.Path(c.router, "unread")) +} diff --git a/ui/user.go b/ui/user.go deleted file mode 100644 index b607a85..0000000 --- a/ui/user.go +++ /dev/null @@ -1,239 +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" -) - -// ShowUsers shows the list of users. -func (c *Controller) ShowUsers(ctx *handler.Context, request *handler.Request, response *handler.Response) { - user := ctx.LoggedUser() - - if !user.IsAdmin { - response.HTML().Forbidden() - return - } - - args, err := c.getCommonTemplateArgs(ctx) - if err != nil { - response.HTML().ServerError(err) - return - } - - users, err := c.store.Users() - if err != nil { - response.HTML().ServerError(err) - return - } - - users.UseTimezone(user.Timezone) - response.HTML().Render("users", ctx.UserLanguage(), args.Merge(tplParams{ - "users": users, - "menu": "settings", - })) -} - -// CreateUser shows the user creation form. -func (c *Controller) CreateUser(ctx *handler.Context, request *handler.Request, response *handler.Response) { - user := ctx.LoggedUser() - - if !user.IsAdmin { - response.HTML().Forbidden() - return - } - - args, err := c.getCommonTemplateArgs(ctx) - if err != nil { - response.HTML().ServerError(err) - return - } - - response.HTML().Render("create_user", ctx.UserLanguage(), args.Merge(tplParams{ - "menu": "settings", - "form": &form.UserForm{}, - })) -} - -// SaveUser validate and save the new user into the database. -func (c *Controller) SaveUser(ctx *handler.Context, request *handler.Request, response *handler.Response) { - user := ctx.LoggedUser() - - if !user.IsAdmin { - response.HTML().Forbidden() - return - } - - args, err := c.getCommonTemplateArgs(ctx) - if err != nil { - response.HTML().ServerError(err) - return - } - - userForm := form.NewUserForm(request.Request()) - if err := userForm.ValidateCreation(); err != nil { - response.HTML().Render("create_user", ctx.UserLanguage(), args.Merge(tplParams{ - "menu": "settings", - "form": userForm, - "errorMessage": err.Error(), - })) - return - } - - if c.store.UserExists(userForm.Username) { - response.HTML().Render("create_user", ctx.UserLanguage(), args.Merge(tplParams{ - "menu": "settings", - "form": userForm, - "errorMessage": "This user already exists.", - })) - return - } - - newUser := userForm.ToUser() - if err := c.store.CreateUser(newUser); err != nil { - logger.Error("[Controller:SaveUser] %v", err) - response.HTML().Render("edit_user", ctx.UserLanguage(), args.Merge(tplParams{ - "menu": "settings", - "form": userForm, - "errorMessage": "Unable to create this user.", - })) - return - } - - response.Redirect(ctx.Route("users")) -} - -// EditUser shows the form to edit a user. -func (c *Controller) EditUser(ctx *handler.Context, request *handler.Request, response *handler.Response) { - user := ctx.LoggedUser() - - if !user.IsAdmin { - response.HTML().Forbidden() - return - } - - args, err := c.getCommonTemplateArgs(ctx) - if err != nil { - response.HTML().ServerError(err) - return - } - - selectedUser, err := c.getUserFromURL(ctx, request, response) - if err != nil { - return - } - - response.HTML().Render("edit_user", ctx.UserLanguage(), args.Merge(tplParams{ - "menu": "settings", - "selected_user": selectedUser, - "form": &form.UserForm{ - Username: selectedUser.Username, - IsAdmin: selectedUser.IsAdmin, - }, - })) -} - -// UpdateUser validate and update a user. -func (c *Controller) UpdateUser(ctx *handler.Context, request *handler.Request, response *handler.Response) { - user := ctx.LoggedUser() - - if !user.IsAdmin { - response.HTML().Forbidden() - return - } - - args, err := c.getCommonTemplateArgs(ctx) - if err != nil { - response.HTML().ServerError(err) - return - } - - selectedUser, err := c.getUserFromURL(ctx, request, response) - if err != nil { - return - } - - userForm := form.NewUserForm(request.Request()) - if err := userForm.ValidateModification(); err != nil { - response.HTML().Render("edit_user", ctx.UserLanguage(), args.Merge(tplParams{ - "menu": "settings", - "selected_user": selectedUser, - "form": userForm, - "errorMessage": err.Error(), - })) - return - } - - if c.store.AnotherUserExists(selectedUser.ID, userForm.Username) { - response.HTML().Render("edit_user", ctx.UserLanguage(), args.Merge(tplParams{ - "menu": "settings", - "selected_user": selectedUser, - "form": userForm, - "errorMessage": "This user already exists.", - })) - return - } - - userForm.Merge(selectedUser) - if err := c.store.UpdateUser(selectedUser); err != nil { - logger.Error("[Controller:UpdateUser] %v", err) - response.HTML().Render("edit_user", ctx.UserLanguage(), args.Merge(tplParams{ - "menu": "settings", - "selected_user": selectedUser, - "form": userForm, - "errorMessage": "Unable to update this user.", - })) - return - } - - response.Redirect(ctx.Route("users")) -} - -// RemoveUser deletes a user from the database. -func (c *Controller) RemoveUser(ctx *handler.Context, request *handler.Request, response *handler.Response) { - user := ctx.LoggedUser() - if !user.IsAdmin { - response.HTML().Forbidden() - return - } - - selectedUser, err := c.getUserFromURL(ctx, request, response) - if err != nil { - return - } - - if err := c.store.RemoveUser(selectedUser.ID); err != nil { - response.HTML().ServerError(err) - return - } - - response.Redirect(ctx.Route("users")) -} - -func (c *Controller) getUserFromURL(ctx *handler.Context, request *handler.Request, response *handler.Response) (*model.User, error) { - userID, err := request.IntegerParam("userID") - if err != nil { - response.HTML().BadRequest(err) - return nil, err - } - - user, err := c.store.UserByID(userID) - if err != nil { - response.HTML().ServerError(err) - return nil, err - } - - if user == nil { - response.HTML().NotFound() - return nil, errors.New("User not found") - } - - return user, nil -} diff --git a/ui/user_create.go b/ui/user_create.go new file mode 100644 index 0000000..4835863 --- /dev/null +++ b/ui/user_create.go @@ -0,0 +1,40 @@ +// 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/form" + "github.com/miniflux/miniflux/ui/session" + "github.com/miniflux/miniflux/ui/view" +) + +// CreateUser shows the user creation form. +func (c *Controller) CreateUser(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 + } + + if !user.IsAdmin { + html.Forbidden(w) + return + } + + view.Set("form", &form.UserForm{}) + view.Set("menu", "settings") + view.Set("user", user) + view.Set("countUnread", c.store.CountUnreadEntries(user.ID)) + + html.OK(w, view.Render("create_user")) +} diff --git a/ui/user_edit.go b/ui/user_edit.go new file mode 100644 index 0000000..648e4d6 --- /dev/null +++ b/ui/user_edit.go @@ -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/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" +) + +// EditUser shows the form to edit a user. +func (c *Controller) EditUser(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 + } + + if !user.IsAdmin { + html.Forbidden(w) + return + } + + userID, err := request.IntParam(r, "userID") + if err != nil { + html.BadRequest(w, err) + return + } + + selectedUser, err := c.store.UserByID(userID) + if err != nil { + html.ServerError(w, err) + return + } + + if selectedUser == nil { + html.NotFound(w) + return + } + + userForm := &form.UserForm{ + Username: selectedUser.Username, + IsAdmin: selectedUser.IsAdmin, + } + + view.Set("form", userForm) + view.Set("selected_user", selectedUser) + view.Set("menu", "settings") + view.Set("user", user) + view.Set("countUnread", c.store.CountUnreadEntries(user.ID)) + + html.OK(w, view.Render("edit_user")) +} diff --git a/ui/user_list.go b/ui/user_list.go new file mode 100644 index 0000000..4bab764 --- /dev/null +++ b/ui/user_list.go @@ -0,0 +1,47 @@ +// 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" +) + +// ShowUsers renders the list of users. +func (c *Controller) ShowUsers(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 + } + + if !user.IsAdmin { + html.Forbidden(w) + return + } + + users, err := c.store.Users() + if err != nil { + html.ServerError(w, err) + return + } + + users.UseTimezone(user.Timezone) + + view.Set("users", users) + view.Set("menu", "settings") + view.Set("user", user) + view.Set("countUnread", c.store.CountUnreadEntries(user.ID)) + + html.OK(w, view.Render("users")) +} diff --git a/ui/user_remove.go b/ui/user_remove.go new file mode 100644 index 0000000..0b9113b --- /dev/null +++ b/ui/user_remove.go @@ -0,0 +1,55 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package 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" +) + +// RemoveUser deletes a user from the database. +func (c *Controller) RemoveUser(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 + } + + if !user.IsAdmin { + html.Forbidden(w) + return + } + + userID, err := request.IntParam(r, "userID") + if err != nil { + html.BadRequest(w, err) + return + } + + selectedUser, err := c.store.UserByID(userID) + if err != nil { + html.ServerError(w, err) + return + } + + if selectedUser == nil { + html.NotFound(w) + return + } + + if err := c.store.RemoveUser(selectedUser.ID); err != nil { + html.ServerError(w, err) + return + } + + response.Redirect(w, r, route.Path(c.router, "users")) +} diff --git a/ui/user_save.go b/ui/user_save.go new file mode 100644 index 0000000..a0057e1 --- /dev/null +++ b/ui/user_save.go @@ -0,0 +1,65 @@ +// 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/ui/form" + "github.com/miniflux/miniflux/ui/session" + "github.com/miniflux/miniflux/ui/view" +) + +// SaveUser validate and save the new user into the database. +func (c *Controller) SaveUser(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 + } + + if !user.IsAdmin { + html.Forbidden(w) + return + } + + userForm := form.NewUserForm(r) + + sess := session.New(c.store, ctx) + view := view.New(c.tpl, ctx, sess) + view.Set("menu", "settings") + view.Set("user", user) + view.Set("countUnread", c.store.CountUnreadEntries(user.ID)) + view.Set("form", userForm) + + if err := userForm.ValidateCreation(); err != nil { + view.Set("errorMessage", err.Error()) + html.OK(w, view.Render("create_user")) + return + } + + if c.store.UserExists(userForm.Username) { + view.Set("errorMessage", "This user already exists.") + html.OK(w, view.Render("create_user")) + return + } + + newUser := userForm.ToUser() + if err := c.store.CreateUser(newUser); err != nil { + logger.Error("[Controller:SaveUser] %v", err) + view.Set("errorMessage", "Unable to create this user.") + html.OK(w, view.Render("create_user")) + return + } + + response.Redirect(w, r, route.Path(c.router, "users")) +} diff --git a/ui/user_update.go b/ui/user_update.go new file mode 100644 index 0000000..d14f8c9 --- /dev/null +++ b/ui/user_update.go @@ -0,0 +1,84 @@ +// 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" +) + +// UpdateUser validate and update a user. +func (c *Controller) UpdateUser(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 + } + + if !user.IsAdmin { + html.Forbidden(w) + return + } + + userID, err := request.IntParam(r, "userID") + if err != nil { + html.BadRequest(w, err) + return + } + + selectedUser, err := c.store.UserByID(userID) + if err != nil { + html.ServerError(w, err) + return + } + + if selectedUser == nil { + html.NotFound(w) + return + } + + userForm := form.NewUserForm(r) + + sess := session.New(c.store, ctx) + view := view.New(c.tpl, ctx, sess) + view.Set("menu", "settings") + view.Set("user", user) + view.Set("countUnread", c.store.CountUnreadEntries(user.ID)) + view.Set("selected_user", selectedUser) + view.Set("form", userForm) + + if err := userForm.ValidateModification(); err != nil { + view.Set("errorMessage", err.Error()) + html.OK(w, view.Render("edit_user")) + return + } + + if c.store.AnotherUserExists(selectedUser.ID, userForm.Username) { + view.Set("errorMessage", "This user already exists.") + html.OK(w, view.Render("edit_user")) + return + } + + userForm.Merge(selectedUser) + if err := c.store.UpdateUser(selectedUser); err != nil { + logger.Error("[Controller:UpdateUser] %v", err) + view.Set("errorMessage", "Unable to update this user.") + html.OK(w, view.Render("edit_user")) + return + } + + response.Redirect(w, r, route.Path(c.router, "users")) +} diff --git a/ui/view/view.go b/ui/view/view.go new file mode 100644 index 0000000..a1c6646 --- /dev/null +++ b/ui/view/view.go @@ -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 view + +import ( + "github.com/miniflux/miniflux/http/context" + "github.com/miniflux/miniflux/template" + "github.com/miniflux/miniflux/ui/session" +) + +// View wraps template argument building. +type View struct { + tpl *template.Engine + ctx *context.Context + params map[string]interface{} +} + +// Set adds a new template argument. +func (v *View) Set(param string, value interface{}) *View { + v.params[param] = value + return v +} + +// Render executes the template with arguments. +func (v *View) Render(template string) []byte { + return v.tpl.Render(template, v.ctx.UserLanguage(), v.params) +} + +// New returns a new view with default parameters. +func New(tpl *template.Engine, ctx *context.Context, sess *session.Session) *View { + b := &View{tpl, ctx, make(map[string]interface{})} + b.params["menu"] = "" + b.params["csrf"] = ctx.CSRF() + b.params["flashMessage"] = sess.FlashMessage() + b.params["flashErrorMessage"] = sess.FlashErrorMessage() + return b +} -- cgit v1.2.3