From 1f58b37a5e86603b16e137031c36f37580e9d410 Mon Sep 17 00:00:00 2001 From: Frédéric Guillot Date: Sun, 7 Oct 2018 18:42:43 -0700 Subject: Refactor HTTP response builder --- api/category.go | 29 ++-- api/entry.go | 44 ++--- api/feed.go | 46 +++--- api/icon.go | 7 +- api/opml.go | 8 +- api/subscription.go | 7 +- api/user.go | 54 +++---- daemon/routes.go | 1 - fever/fever.go | 22 +-- http/response/builder.go | 134 +++++++++++++++ http/response/builder_test.go | 351 ++++++++++++++++++++++++++++++++++++++++ http/response/doc.go | 10 ++ http/response/html/doc.go | 10 ++ http/response/html/html.go | 92 ++++++----- http/response/html/html_test.go | 212 ++++++++++++++++++++++++ http/response/json/doc.go | 10 ++ http/response/json/json.go | 127 ++++++++------- http/response/json/json_test.go | 313 +++++++++++++++++++++++++++++++++++ http/response/response.go | 63 -------- http/response/xml/doc.go | 10 ++ http/response/xml/xml.go | 25 +-- http/response/xml/xml_test.go | 83 ++++++++++ locale/translations.go | 8 +- locale/translations/en_US.json | 2 +- locale/translations/fr_FR.json | 2 +- middleware/app_session.go | 5 +- middleware/basic_auth.go | 8 +- middleware/common_headers.go | 25 --- middleware/fever.go | 2 +- middleware/header_config.go | 5 + middleware/user_session.go | 4 +- ui/about.go | 2 +- ui/bookmark_entries.go | 8 +- ui/category_create.go | 2 +- ui/category_edit.go | 8 +- ui/category_entries.go | 12 +- ui/category_list.go | 6 +- ui/category_remove.go | 11 +- ui/category_save.go | 9 +- ui/category_update.go | 11 +- ui/entry_bookmark.go | 14 +- ui/entry_category.go | 14 +- ui/entry_feed.go | 14 +- ui/entry_read.go | 10 +- ui/entry_save.go | 9 +- ui/entry_scraper.go | 9 +- ui/entry_search.go | 12 +- ui/entry_toggle_bookmark.go | 4 +- ui/entry_unread.go | 17 +- ui/entry_update_status.go | 9 +- ui/feed_edit.go | 10 +- ui/feed_entries.go | 12 +- ui/feed_icon.go | 13 +- ui/feed_list.go | 4 +- ui/feed_refresh.go | 9 +- ui/feed_remove.go | 5 +- ui/feed_update.go | 13 +- ui/history_entries.go | 8 +- ui/history_flush.go | 5 +- ui/integration_pocket.go | 21 ++- ui/integration_show.go | 6 +- ui/integration_update.go | 15 +- ui/login_check.go | 7 +- ui/login_show.go | 7 +- ui/logout.go | 7 +- ui/oauth2.go | 2 +- ui/oauth2_callback.go | 31 ++-- ui/oauth2_redirect.go | 10 +- ui/oauth2_unlink.go | 15 +- ui/opml_export.go | 4 +- ui/opml_import.go | 4 +- ui/opml_upload.go | 9 +- ui/proxy.go | 23 +-- ui/search_entries.go | 8 +- ui/session_list.go | 8 +- ui/session_remove.go | 4 +- ui/settings_show.go | 6 +- ui/settings_update.go | 9 +- ui/static_app_icon.go | 26 +-- ui/static_favicon.go | 25 ++- ui/static_javascript.go | 14 +- ui/static_stylesheet.go | 16 +- ui/subscription_add.go | 4 +- ui/subscription_bookmarklet.go | 4 +- ui/subscription_choose.go | 9 +- ui/subscription_submit.go | 7 +- ui/unread_entries.go | 8 +- ui/unread_mark_all_read.go | 4 +- ui/user_create.go | 4 +- ui/user_edit.go | 10 +- ui/user_list.go | 8 +- ui/user_remove.go | 13 +- ui/user_save.go | 7 +- ui/user_update.go | 13 +- 94 files changed, 1702 insertions(+), 645 deletions(-) create mode 100644 http/response/builder.go create mode 100644 http/response/builder_test.go create mode 100644 http/response/doc.go create mode 100644 http/response/html/doc.go create mode 100644 http/response/html/html_test.go create mode 100644 http/response/json/doc.go create mode 100644 http/response/json/json_test.go delete mode 100644 http/response/response.go create mode 100644 http/response/xml/doc.go create mode 100644 http/response/xml/xml_test.go delete mode 100644 middleware/common_headers.go diff --git a/api/category.go b/api/category.go index b8699e2..6343517 100644 --- a/api/category.go +++ b/api/category.go @@ -16,29 +16,28 @@ import ( func (c *Controller) CreateCategory(w http.ResponseWriter, r *http.Request) { category, err := decodeCategoryPayload(r.Body) if err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } userID := request.UserID(r) category.UserID = userID if err := category.ValidateCategoryCreation(); err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } if c, err := c.store.CategoryByTitle(userID, category.Title); err != nil || c != nil { - json.BadRequest(w, errors.New("This category already exists")) + json.BadRequest(w, r, errors.New("This category already exists")) return } - err = c.store.CreateCategory(category) - if err != nil { - json.ServerError(w, err) + if err := c.store.CreateCategory(category); err != nil { + json.ServerError(w, r, err) return } - json.Created(w, category) + json.Created(w, r, category) } // UpdateCategory is the API handler to update a category. @@ -47,31 +46,31 @@ func (c *Controller) UpdateCategory(w http.ResponseWriter, r *http.Request) { category, err := decodeCategoryPayload(r.Body) if err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } category.UserID = request.UserID(r) category.ID = categoryID if err := category.ValidateCategoryModification(); err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } err = c.store.UpdateCategory(category) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } - json.Created(w, category) + json.Created(w, r, category) } // GetCategories is the API handler to get a list of categories for a given user. func (c *Controller) GetCategories(w http.ResponseWriter, r *http.Request) { categories, err := c.store.Categories(request.UserID(r)) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -84,14 +83,14 @@ func (c *Controller) RemoveCategory(w http.ResponseWriter, r *http.Request) { categoryID := request.RouteInt64Param(r, "categoryID") if !c.store.CategoryExists(userID, categoryID) { - json.NotFound(w, errors.New("Category not found")) + json.NotFound(w, r) return } if err := c.store.RemoveCategory(userID, categoryID); err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } - json.NoContent(w) + json.NoContent(w, r) } diff --git a/api/entry.go b/api/entry.go index a1ea87f..c727785 100644 --- a/api/entry.go +++ b/api/entry.go @@ -26,12 +26,12 @@ func (c *Controller) GetFeedEntry(w http.ResponseWriter, r *http.Request) { entry, err := builder.GetEntry() if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } if entry == nil { - json.NotFound(w, errors.New("Entry not found")) + json.NotFound(w, r) return } @@ -46,12 +46,12 @@ func (c *Controller) GetEntry(w http.ResponseWriter, r *http.Request) { entry, err := builder.GetEntry() if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } if entry == nil { - json.NotFound(w, errors.New("Entry not found")) + json.NotFound(w, r) return } @@ -65,27 +65,27 @@ func (c *Controller) GetFeedEntries(w http.ResponseWriter, r *http.Request) { status := request.QueryStringParam(r, "status", "") if status != "" { if err := model.ValidateEntryStatus(status); err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } } order := request.QueryStringParam(r, "order", model.DefaultSortingOrder) if err := model.ValidateEntryOrder(order); err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } direction := request.QueryStringParam(r, "direction", model.DefaultSortingDirection) if err := model.ValidateDirection(direction); err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } limit := request.QueryIntParam(r, "limit", 100) offset := request.QueryIntParam(r, "offset", 0) if err := model.ValidateRange(offset, limit); err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } @@ -100,13 +100,13 @@ func (c *Controller) GetFeedEntries(w http.ResponseWriter, r *http.Request) { entries, err := builder.GetEntries() if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } count, err := builder.CountEntries() if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -118,27 +118,27 @@ func (c *Controller) GetEntries(w http.ResponseWriter, r *http.Request) { status := request.QueryStringParam(r, "status", "") if status != "" { if err := model.ValidateEntryStatus(status); err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } } order := request.QueryStringParam(r, "order", model.DefaultSortingOrder) if err := model.ValidateEntryOrder(order); err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } direction := request.QueryStringParam(r, "direction", model.DefaultSortingDirection) if err := model.ValidateDirection(direction); err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } limit := request.QueryIntParam(r, "limit", 100) offset := request.QueryIntParam(r, "offset", 0) if err := model.ValidateRange(offset, limit); err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } @@ -152,13 +152,13 @@ func (c *Controller) GetEntries(w http.ResponseWriter, r *http.Request) { entries, err := builder.GetEntries() if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } count, err := builder.CountEntries() if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -169,32 +169,32 @@ func (c *Controller) GetEntries(w http.ResponseWriter, r *http.Request) { func (c *Controller) SetEntryStatus(w http.ResponseWriter, r *http.Request) { entryIDs, status, err := decodeEntryStatusPayload(r.Body) if err != nil { - json.BadRequest(w, errors.New("Invalid JSON payload")) + json.BadRequest(w , r, errors.New("Invalid JSON payload")) return } if err := model.ValidateEntryStatus(status); err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } if err := c.store.SetEntriesStatus(request.UserID(r), entryIDs, status); err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } - json.NoContent(w) + json.NoContent(w, r) } // ToggleBookmark is the API handler to toggle bookmark status. func (c *Controller) ToggleBookmark(w http.ResponseWriter, r *http.Request) { entryID := request.RouteInt64Param(r, "entryID") if err := c.store.ToggleBookmark(request.UserID(r), entryID); err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } - json.NoContent(w) + json.NoContent(w, r) } func configureFilters(builder *storage.EntryQueryBuilder, r *http.Request) { diff --git a/api/feed.go b/api/feed.go index d193d2e..914ba0d 100644 --- a/api/feed.go +++ b/api/feed.go @@ -16,29 +16,29 @@ import ( func (c *Controller) CreateFeed(w http.ResponseWriter, r *http.Request) { feedInfo, err := decodeFeedCreationPayload(r.Body) if err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } if feedInfo.FeedURL == "" { - json.BadRequest(w, errors.New("The feed_url is required")) + json.BadRequest(w, r, errors.New("The feed_url is required")) return } if feedInfo.CategoryID <= 0 { - json.BadRequest(w, errors.New("The category_id is required")) + json.BadRequest(w, r, errors.New("The category_id is required")) return } userID := request.UserID(r) if c.store.FeedURLExists(userID, feedInfo.FeedURL) { - json.BadRequest(w, errors.New("This feed_url already exists")) + json.BadRequest(w, r, errors.New("This feed_url already exists")) return } if !c.store.CategoryExists(userID, feedInfo.CategoryID) { - json.BadRequest(w, errors.New("This category_id doesn't exists or doesn't belongs to this user")) + json.BadRequest(w, r, errors.New("This category_id doesn't exists or doesn't belongs to this user")) return } @@ -52,7 +52,7 @@ func (c *Controller) CreateFeed(w http.ResponseWriter, r *http.Request) { feedInfo.Password, ) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -60,7 +60,7 @@ func (c *Controller) CreateFeed(w http.ResponseWriter, r *http.Request) { FeedID int64 `json:"feed_id"` } - json.Created(w, &result{FeedID: feed.ID}) + json.Created(w, r, &result{FeedID: feed.ID}) } // RefreshFeed is the API handler to refresh a feed. @@ -69,17 +69,17 @@ func (c *Controller) RefreshFeed(w http.ResponseWriter, r *http.Request) { userID := request.UserID(r) if !c.store.FeedExists(userID, feedID) { - json.NotFound(w, errors.New("Unable to find this feed")) + json.NotFound(w, r) return } err := c.feedHandler.RefreshFeed(userID, feedID) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } - json.NoContent(w) + json.NoContent(w, r) } // UpdateFeed is the API handler that is used to update a feed. @@ -87,7 +87,7 @@ func (c *Controller) UpdateFeed(w http.ResponseWriter, r *http.Request) { feedID := request.RouteInt64Param(r, "feedID") feedChanges, err := decodeFeedModificationPayload(r.Body) if err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } @@ -95,41 +95,41 @@ func (c *Controller) UpdateFeed(w http.ResponseWriter, r *http.Request) { originalFeed, err := c.store.FeedByID(userID, feedID) if err != nil { - json.NotFound(w, errors.New("Unable to find this feed")) + json.NotFound(w, r) return } if originalFeed == nil { - json.NotFound(w, errors.New("Feed not found")) + json.NotFound(w, r) return } feedChanges.Update(originalFeed) if !c.store.CategoryExists(userID, originalFeed.Category.ID) { - json.BadRequest(w, errors.New("This category_id doesn't exists or doesn't belongs to this user")) + json.BadRequest(w, r, errors.New("This category_id doesn't exists or doesn't belongs to this user")) return } if err := c.store.UpdateFeed(originalFeed); err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } originalFeed, err = c.store.FeedByID(userID, feedID) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } - json.Created(w, originalFeed) + json.Created(w, r, originalFeed) } // GetFeeds is the API handler that get all feeds that belongs to the given user. func (c *Controller) GetFeeds(w http.ResponseWriter, r *http.Request) { feeds, err := c.store.Feeds(request.UserID(r)) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -141,12 +141,12 @@ func (c *Controller) GetFeed(w http.ResponseWriter, r *http.Request) { feedID := request.RouteInt64Param(r, "feedID") feed, err := c.store.FeedByID(request.UserID(r), feedID) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } if feed == nil { - json.NotFound(w, errors.New("Feed not found")) + json.NotFound(w, r) return } @@ -159,14 +159,14 @@ func (c *Controller) RemoveFeed(w http.ResponseWriter, r *http.Request) { userID := request.UserID(r) if !c.store.FeedExists(userID, feedID) { - json.NotFound(w, errors.New("Feed not found")) + json.NotFound(w, r) return } if err := c.store.RemoveFeed(userID, feedID); err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } - json.NoContent(w) + json.NoContent(w, r) } diff --git a/api/icon.go b/api/icon.go index de01fad..1954769 100644 --- a/api/icon.go +++ b/api/icon.go @@ -5,7 +5,6 @@ package api // import "miniflux.app/api" import ( - "errors" "net/http" "miniflux.app/http/request" @@ -17,18 +16,18 @@ func (c *Controller) FeedIcon(w http.ResponseWriter, r *http.Request) { feedID := request.RouteInt64Param(r, "feedID") if !c.store.HasIcon(feedID) { - json.NotFound(w, errors.New("This feed doesn't have any icon")) + json.NotFound(w, r) return } icon, err := c.store.IconByFeedID(request.UserID(r), feedID) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } if icon == nil { - json.NotFound(w, errors.New("This feed doesn't have any icon")) + json.NotFound(w, r) return } diff --git a/api/opml.go b/api/opml.go index eb214f3..199c0e7 100644 --- a/api/opml.go +++ b/api/opml.go @@ -18,11 +18,11 @@ func (c *Controller) Export(w http.ResponseWriter, r *http.Request) { opmlHandler := opml.NewHandler(c.store) opml, err := opmlHandler.Export(request.UserID(r)) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } - xml.OK(w, opml) + xml.OK(w, r, opml) } // Import is the API handler that import an OPML file. @@ -31,9 +31,9 @@ func (c *Controller) Import(w http.ResponseWriter, r *http.Request) { err := opmlHandler.Import(request.UserID(r), r.Body) defer r.Body.Close() if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } - json.Created(w, map[string]string{"message": "Feeds imported successfully"}) + json.Created(w, r, map[string]string{"message": "Feeds imported successfully"}) } diff --git a/api/subscription.go b/api/subscription.go index ff4c7cf..701759d 100644 --- a/api/subscription.go +++ b/api/subscription.go @@ -5,7 +5,6 @@ package api // import "miniflux.app/api" import ( - "fmt" "net/http" "miniflux.app/http/response/json" @@ -16,7 +15,7 @@ import ( func (c *Controller) GetSubscriptions(w http.ResponseWriter, r *http.Request) { subscriptionInfo, err := decodeURLPayload(r.Body) if err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } @@ -27,12 +26,12 @@ func (c *Controller) GetSubscriptions(w http.ResponseWriter, r *http.Request) { subscriptionInfo.Password, ) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } if subscriptions == nil { - json.NotFound(w, fmt.Errorf("No subscription found")) + json.NotFound(w, r) return } diff --git a/api/user.go b/api/user.go index b9274bb..db1e049 100644 --- a/api/user.go +++ b/api/user.go @@ -16,7 +16,7 @@ import ( func (c *Controller) CurrentUser(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -26,85 +26,85 @@ func (c *Controller) CurrentUser(w http.ResponseWriter, r *http.Request) { // CreateUser is the API handler to create a new user. func (c *Controller) CreateUser(w http.ResponseWriter, r *http.Request) { if !request.IsAdminUser(r) { - json.Forbidden(w) + json.Forbidden(w, r) return } user, err := decodeUserCreationPayload(r.Body) if err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } if err := user.ValidateUserCreation(); err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } if c.store.UserExists(user.Username) { - json.BadRequest(w, errors.New("This user already exists")) + json.BadRequest(w, r, errors.New("This user already exists")) return } err = c.store.CreateUser(user) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } user.Password = "" - json.Created(w, user) + json.Created(w, r, user) } // UpdateUser is the API handler to update the given user. func (c *Controller) UpdateUser(w http.ResponseWriter, r *http.Request) { if !request.IsAdminUser(r) { - json.Forbidden(w) + json.Forbidden(w, r) return } userID := request.RouteInt64Param(r, "userID") userChanges, err := decodeUserModificationPayload(r.Body) if err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } originalUser, err := c.store.UserByID(userID) if err != nil { - json.BadRequest(w, errors.New("Unable to fetch this user from the database")) + json.BadRequest(w, r, errors.New("Unable to fetch this user from the database")) return } if originalUser == nil { - json.NotFound(w, errors.New("User not found")) + json.NotFound(w, r) return } userChanges.Update(originalUser) if err := originalUser.ValidateUserModification(); err != nil { - json.BadRequest(w, err) + json.BadRequest(w, r, err) return } if err = c.store.UpdateUser(originalUser); err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } - json.Created(w, originalUser) + json.Created(w, r, originalUser) } // Users is the API handler to get the list of users. func (c *Controller) Users(w http.ResponseWriter, r *http.Request) { if !request.IsAdminUser(r) { - json.Forbidden(w) + json.Forbidden(w, r) return } users, err := c.store.Users() if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -115,19 +115,19 @@ func (c *Controller) Users(w http.ResponseWriter, r *http.Request) { // UserByID is the API handler to fetch the given user by the ID. func (c *Controller) UserByID(w http.ResponseWriter, r *http.Request) { if !request.IsAdminUser(r) { - json.Forbidden(w) + json.Forbidden(w, r) return } userID := request.RouteInt64Param(r, "userID") user, err := c.store.UserByID(userID) if err != nil { - json.BadRequest(w, errors.New("Unable to fetch this user from the database")) + json.BadRequest(w, r, errors.New("Unable to fetch this user from the database")) return } if user == nil { - json.NotFound(w, errors.New("User not found")) + json.NotFound(w, r) return } @@ -138,19 +138,19 @@ func (c *Controller) UserByID(w http.ResponseWriter, r *http.Request) { // UserByUsername is the API handler to fetch the given user by the username. func (c *Controller) UserByUsername(w http.ResponseWriter, r *http.Request) { if !request.IsAdminUser(r) { - json.Forbidden(w) + json.Forbidden(w, r) return } username := request.RouteStringParam(r, "username") user, err := c.store.UserByUsername(username) if err != nil { - json.BadRequest(w, errors.New("Unable to fetch this user from the database")) + json.BadRequest(w, r, errors.New("Unable to fetch this user from the database")) return } if user == nil { - json.NotFound(w, errors.New("User not found")) + json.NotFound(w, r) return } @@ -160,26 +160,26 @@ func (c *Controller) UserByUsername(w http.ResponseWriter, r *http.Request) { // RemoveUser is the API handler to remove an existing user. func (c *Controller) RemoveUser(w http.ResponseWriter, r *http.Request) { if !request.IsAdminUser(r) { - json.Forbidden(w) + json.Forbidden(w, r) return } userID := request.RouteInt64Param(r, "userID") user, err := c.store.UserByID(userID) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } if user == nil { - json.NotFound(w, errors.New("User not found")) + json.NotFound(w, r) return } if err := c.store.RemoveUser(user.ID); err != nil { - json.BadRequest(w, errors.New("Unable to remove this user from the database")) + json.BadRequest(w, r, errors.New("Unable to remove this user from the database")) return } - json.NoContent(w) + json.NoContent(w, r) } diff --git a/daemon/routes.go b/daemon/routes.go index 88b8c20..f89b0be 100644 --- a/daemon/routes.go +++ b/daemon/routes.go @@ -35,7 +35,6 @@ func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handle router.Use(middleware.ClientIP) 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")) diff --git a/fever/fever.go b/fever/fever.go index b7c8bc8..cb65874 100644 --- a/fever/fever.go +++ b/fever/fever.go @@ -184,13 +184,13 @@ func (c *Controller) handleGroups(w http.ResponseWriter, r *http.Request) { categories, err := c.store.Categories(userID) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } feeds, err := c.store.Feeds(userID) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -234,7 +234,7 @@ func (c *Controller) handleFeeds(w http.ResponseWriter, r *http.Request) { feeds, err := c.store.Feeds(userID) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -287,7 +287,7 @@ func (c *Controller) handleFavicons(w http.ResponseWriter, r *http.Request) { icons, err := c.store.Icons(userID) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -371,7 +371,7 @@ func (c *Controller) handleItems(w http.ResponseWriter, r *http.Request) { entries, err := builder.GetEntries() if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -379,7 +379,7 @@ func (c *Controller) handleItems(w http.ResponseWriter, r *http.Request) { builder.WithoutStatus(model.EntryStatusRemoved) result.Total, err = builder.CountEntries() if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -427,7 +427,7 @@ func (c *Controller) handleUnreadItems(w http.ResponseWriter, r *http.Request) { builder.WithStatus(model.EntryStatusUnread) entries, err := builder.GetEntries() if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -459,7 +459,7 @@ func (c *Controller) handleSavedItems(w http.ResponseWriter, r *http.Request) { entryIDs, err := builder.GetEntryIDs() if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -493,7 +493,7 @@ func (c *Controller) handleWriteItems(w http.ResponseWriter, r *http.Request) { entry, err := builder.GetEntry() if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -511,13 +511,13 @@ func (c *Controller) handleWriteItems(w http.ResponseWriter, r *http.Request) { case "saved", "unsaved": logger.Debug("[Fever] Mark entry #%d as saved/unsaved", entryID) if err := c.store.ToggleBookmark(userID, entryID); err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } settings, err := c.store.Integration(userID) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } diff --git a/http/response/builder.go b/http/response/builder.go new file mode 100644 index 0000000..f17c62e --- /dev/null +++ b/http/response/builder.go @@ -0,0 +1,134 @@ +// 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 "miniflux.app/http/response" + +import ( + "compress/flate" + "compress/gzip" + "fmt" + "net/http" + "strings" + "time" +) + +const compressionThreshold = 1024 + +// Builder generates HTTP responses. +type Builder struct { + w http.ResponseWriter + r *http.Request + statusCode int + headers map[string]string + enableCompression bool + body interface{} +} + +// WithStatus uses the given status code to build the response. +func (b *Builder) WithStatus(statusCode int) *Builder { + b.statusCode = statusCode + return b +} + +// WithHeader adds the given HTTP header to the response. +func (b *Builder) WithHeader(key, value string) *Builder { + b.headers[key] = value + return b +} + +// WithBody uses the given body to build the response. +func (b *Builder) WithBody(body interface{}) *Builder { + b.body = body + return b +} + +// WithAttachment forces the document to be downloaded by the web browser. +func (b *Builder) WithAttachment(filename string) *Builder { + b.headers["Content-Disposition"] = fmt.Sprintf("attachment; filename=%s", filename) + return b +} + +// WithoutCompression disables HTTP compression. +func (b *Builder) WithoutCompression() *Builder { + b.enableCompression = false + return b +} + +// WithCaching adds caching headers to the response. +func (b *Builder) WithCaching(etag string, duration time.Duration, callback func(*Builder)) { + b.headers["ETag"] = etag + b.headers["Cache-Control"] = "public" + b.headers["Expires"] = time.Now().Add(duration).Format(time.RFC1123) + + if etag == b.r.Header.Get("If-None-Match") { + b.statusCode = http.StatusNotModified + b.body = nil + b.Write() + } else { + callback(b) + } +} + +// Write generates the HTTP response. +func (b *Builder) Write() { + if b.body == nil { + b.writeHeaders() + return + } + + switch v := b.body.(type) { + case []byte: + b.compress(v) + case string: + b.compress([]byte(v)) + case error: + b.compress([]byte(v.Error())) + } +} + +func (b *Builder) writeHeaders() { + b.headers["X-XSS-Protection"] = "1; mode=block" + b.headers["X-Content-Type-Options"] = "nosniff" + b.headers["X-Frame-Options"] = "DENY" + b.headers["Content-Security-Policy"] = "default-src 'self'; img-src *; media-src *; frame-src *; child-src *" + + for key, value := range b.headers { + b.w.Header().Set(key, value) + } + + b.w.WriteHeader(b.statusCode) +} + +func (b *Builder) compress(data []byte) { + if b.enableCompression && len(data) > compressionThreshold { + acceptEncoding := b.r.Header.Get("Accept-Encoding") + + switch { + case strings.Contains(acceptEncoding, "gzip"): + b.headers["Content-Encoding"] = "gzip" + b.writeHeaders() + + gzipWriter := gzip.NewWriter(b.w) + defer gzipWriter.Close() + gzipWriter.Write(data) + return + case strings.Contains(acceptEncoding, "deflate"): + b.headers["Content-Encoding"] = "deflate" + b.writeHeaders() + + flateWriter, _ := flate.NewWriter(b.w, -1) + defer flateWriter.Close() + flateWriter.Write(data) + return + } + } + + b.writeHeaders() + b.w.Write(data) +} + +// New creates a new response builder. +func New(w http.ResponseWriter, r *http.Request) *Builder { + return &Builder{w: w, r: r, statusCode: http.StatusOK, headers: make(map[string]string), enableCompression: true} +} diff --git a/http/response/builder_test.go b/http/response/builder_test.go new file mode 100644 index 0000000..f4fef47 --- /dev/null +++ b/http/response/builder_test.go @@ -0,0 +1,351 @@ +// 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 "miniflux.app/http/response" + +import ( + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func TestResponseHasCommonHeaders(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + headers := map[string]string{ + "X-XSS-Protection": "1; mode=block", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "Content-Security-Policy": "default-src 'self'; img-src *; media-src *; frame-src *; child-src *", + } + + for header, expected := range headers { + actual := resp.Header.Get(header) + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } + } +} + +func TestBuildResponseWithCustomStatusCode(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithStatus(http.StatusNotAcceptable).Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusNotAcceptable + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } +} + +func TestBuildResponseWithCustomHeader(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithHeader("X-My-Header", "Value").Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expected := "Value" + actual := resp.Header.Get("X-My-Header") + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } +} + +func TestBuildResponseWithAttachment(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithAttachment("my_file.pdf").Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expected := "attachment; filename=my_file.pdf" + actual := resp.Header.Get("Content-Disposition") + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } +} + +func TestBuildResponseWithError(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithBody(errors.New("Some error")).Write() + }) + + handler.ServeHTTP(w, r) + + expectedBody := `Some error` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } +} + +func TestBuildResponseWithByteBody(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithBody([]byte("body")).Write() + }) + + handler.ServeHTTP(w, r) + + expectedBody := `body` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } +} + +func TestBuildResponseWithCachingEnabled(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithCaching("etag", 1*time.Minute, func(b *Builder) { + b.WithBody("cached body") + b.Write() + }) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusOK + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `cached body` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedHeader := "public" + actualHeader := resp.Header.Get("Cache-Control") + if actualHeader != expectedHeader { + t.Fatalf(`Unexpected cache control header, got %q instead of %q`, actualHeader, expectedHeader) + } + + if resp.Header.Get("Expires") == "" { + t.Fatalf(`Expires header should not be empty`) + } +} + +func TestBuildResponseWithCachingAndEtag(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + r.Header.Set("If-None-Match", "etag") + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithCaching("etag", 1*time.Minute, func(b *Builder) { + b.WithBody("cached body") + b.Write() + }) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusNotModified + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedHeader := "public" + actualHeader := resp.Header.Get("Cache-Control") + if actualHeader != expectedHeader { + t.Fatalf(`Unexpected cache control header, got %q instead of %q`, actualHeader, expectedHeader) + } + + if resp.Header.Get("Expires") == "" { + t.Fatalf(`Expires header should not be empty`) + } +} + +func TestBuildResponseWithGzipCompression(t *testing.T) { + body := strings.Repeat("a", compressionThreshold+1) + r, err := http.NewRequest("GET", "/", nil) + r.Header.Set("Accept-Encoding", "gzip, deflate, br") + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithBody(body).Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expected := "gzip" + actual := resp.Header.Get("Content-Encoding") + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } +} + +func TestBuildResponseWithDeflateCompression(t *testing.T) { + body := strings.Repeat("a", compressionThreshold+1) + r, err := http.NewRequest("GET", "/", nil) + r.Header.Set("Accept-Encoding", "deflate") + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithBody(body).Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expected := "deflate" + actual := resp.Header.Get("Content-Encoding") + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } +} + +func TestBuildResponseWithCompressionDisabled(t *testing.T) { + body := strings.Repeat("a", compressionThreshold+1) + r, err := http.NewRequest("GET", "/", nil) + r.Header.Set("Accept-Encoding", "deflate") + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithBody(body).WithoutCompression().Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expected := "" + actual := resp.Header.Get("Content-Encoding") + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } +} + +func TestBuildResponseWithDeflateCompressionAndSmallPayload(t *testing.T) { + body := strings.Repeat("a", compressionThreshold) + r, err := http.NewRequest("GET", "/", nil) + r.Header.Set("Accept-Encoding", "deflate") + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithBody(body).Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expected := "" + actual := resp.Header.Get("Content-Encoding") + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } +} + +func TestBuildResponseWithoutCompressionHeader(t *testing.T) { + body := strings.Repeat("a", compressionThreshold+1) + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithBody(body).Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expected := "" + actual := resp.Header.Get("Content-Encoding") + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } +} diff --git a/http/response/doc.go b/http/response/doc.go new file mode 100644 index 0000000..007e0fa --- /dev/null +++ b/http/response/doc.go @@ -0,0 +1,10 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +/* + +Package response contains everything related to HTTP responses. + +*/ +package response // import "miniflux.app/http/response" diff --git a/http/response/html/doc.go b/http/response/html/doc.go new file mode 100644 index 0000000..91d3543 --- /dev/null +++ b/http/response/html/doc.go @@ -0,0 +1,10 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +/* + +Package html contains HTML response functions. + +*/ +package html // import "miniflux.app/http/response/html" diff --git a/http/response/html/html.go b/http/response/html/html.go index 65a4649..f173fdb 100644 --- a/http/response/html/html.go +++ b/http/response/html/html.go @@ -1,6 +1,6 @@ // 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. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. package html // import "miniflux.app/http/response/html" @@ -11,48 +11,64 @@ import ( "miniflux.app/logger" ) -// OK writes a standard HTML response. -func OK(w http.ResponseWriter, r *http.Request, b []byte) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - response.Compress(w, r, b) +// OK creates a new HTML response with a 200 status code. +func OK(w http.ResponseWriter, r *http.Request, body interface{}) { + builder := response.New(w, r) + builder.WithHeader("Content-Type", "text/html; charset=utf-8") + builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store") + builder.WithBody(body) + builder.Write() } -// 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")) - } +// ServerError sends an internal error to the client. +func ServerError(w http.ResponseWriter, r *http.Request, err error) { + logger.Error("[HTTP:Internal Server Error] %s => %v", r.URL, err) + + builder := response.New(w, r) + builder.WithStatus(http.StatusInternalServerError) + builder.WithHeader("Content-Type", "text/html; charset=utf-8") + builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store") + builder.WithBody(err) + builder.Write() +} + +// BadRequest sends a bad request error to the client. +func BadRequest(w http.ResponseWriter, r *http.Request, err error) { + logger.Error("[HTTP:Bad Request] %s => %v", r.URL, err) + + builder := response.New(w, r) + builder.WithStatus(http.StatusBadRequest) + builder.WithHeader("Content-Type", "text/html; charset=utf-8") + builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store") + builder.WithBody(err) + builder.Write() } -// 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")) - } +// Forbidden sends a forbidden error to the client. +func Forbidden(w http.ResponseWriter, r *http.Request) { + logger.Error("[HTTP:Forbidden] %s", r.URL) + + builder := response.New(w, r) + builder.WithStatus(http.StatusForbidden) + builder.WithHeader("Content-Type", "text/html; charset=utf-8") + builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store") + builder.WithBody("Access Forbidden") + builder.Write() } -// 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")) +// NotFound sends a page not found error to the client. +func NotFound(w http.ResponseWriter, r *http.Request) { + logger.Error("[HTTP:Not Found] %s", r.URL) + + builder := response.New(w, r) + builder.WithStatus(http.StatusNotFound) + builder.WithHeader("Content-Type", "text/html; charset=utf-8") + builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store") + builder.WithBody("Page Not Found") + builder.Write() } -// 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")) +// Redirect redirects the user to another location. +func Redirect(w http.ResponseWriter, r *http.Request, uri string) { + http.Redirect(w, r, uri, http.StatusFound) } diff --git a/http/response/html/html_test.go b/http/response/html/html_test.go new file mode 100644 index 0000000..91c2b74 --- /dev/null +++ b/http/response/html/html_test.go @@ -0,0 +1,212 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +package html // import "miniflux.app/http/response/html" + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" +) + +func TestOKResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + OK(w, r, "Some HTML") + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusOK + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `Some HTML` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + headers := map[string]string{ + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "no-cache, max-age=0, must-revalidate, no-store", + } + + for header, expected := range headers { + actual := resp.Header.Get(header) + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } + } +} + +func TestServerErrorResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ServerError(w, r, errors.New("Some error")) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusInternalServerError + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `Some error` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "text/html; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestBadRequestResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + BadRequest(w, r, errors.New("Some error")) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusBadRequest + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `Some error` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "text/html; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestForbiddenResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Forbidden(w, r) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusForbidden + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `Access Forbidden` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "text/html; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestNotFoundResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + NotFound(w, r) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusNotFound + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `Page Not Found` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "text/html; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestRedirectResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Redirect(w, r, "/path") + }) + + handler.ServeHTTP(w, r) + + resp := w.Result() + defer resp.Body.Close() + + expectedStatusCode := http.StatusFound + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedResult := "/path" + actualResult := resp.Header.Get("Location") + if actualResult != expectedResult { + t.Fatalf(`Unexpected redirect location, got %q instead of %q`, actualResult, expectedResult) + } +} diff --git a/http/response/json/doc.go b/http/response/json/doc.go new file mode 100644 index 0000000..c2a74c1 --- /dev/null +++ b/http/response/json/doc.go @@ -0,0 +1,10 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +/* + +Package json contains JSON response functions. + +*/ +package json // import "miniflux.app/http/response/json" diff --git a/http/response/json/json.go b/http/response/json/json.go index f19efb0..680a20d 100644 --- a/http/response/json/json.go +++ b/http/response/json/json.go @@ -1,6 +1,6 @@ // 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. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. package json // import "miniflux.app/http/response/json" @@ -13,93 +13,98 @@ import ( "miniflux.app/logger" ) -// OK sends a JSON response with the status code 200. -func OK(w http.ResponseWriter, r *http.Request, v interface{}) { - commonHeaders(w) - response.Compress(w, r, toJSON(v)) +// OK creates a new JSON response with a 200 status code. +func OK(w http.ResponseWriter, r *http.Request, body interface{}) { + builder := response.New(w, r) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.WithBody(toJSON(body)) + builder.Write() } -// 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)) +// Created sends a created response to the client. +func Created(w http.ResponseWriter, r *http.Request, body interface{}) { + builder := response.New(w, r) + builder.WithStatus(http.StatusCreated) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.WithBody(toJSON(body)) + builder.Write() } -// NoContent sends a JSON response with the status code 204. -func NoContent(w http.ResponseWriter) { - commonHeaders(w) - w.WriteHeader(http.StatusNoContent) +// NoContent sends a no content response to the client. +func NoContent(w http.ResponseWriter, r *http.Request) { + builder := response.New(w, r) + builder.WithStatus(http.StatusNoContent) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.Write() } -// 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 an internal error to the client. +func ServerError(w http.ResponseWriter, r *http.Request, err error) { + logger.Error("[HTTP:Internal Server Error] %s => %v", r.URL, err) + + builder := response.New(w, r) + builder.WithStatus(http.StatusInternalServerError) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.WithBody(toJSONError(err)) + builder.Write() } -// 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) +// BadRequest sends a bad request error to the client. +func BadRequest(w http.ResponseWriter, r *http.Request, err error) { + logger.Error("[HTTP:Bad Request] %s => %v", r.URL, err) - if err != nil { - w.Write(encodeError(err)) - } + builder := response.New(w, r) + builder.WithStatus(http.StatusBadRequest) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.WithBody(toJSONError(err)) + builder.Write() } -// 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 not authorized error to the client. +func Unauthorized(w http.ResponseWriter, r *http.Request) { + logger.Error("[HTTP:Unauthorized] %s", r.URL) -// 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"))) + builder := response.New(w, r) + builder.WithStatus(http.StatusUnauthorized) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.WithBody(toJSONError(errors.New("Access Unauthorized"))) + builder.Write() } -// 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) +// Forbidden sends a forbidden error to the client. +func Forbidden(w http.ResponseWriter, r *http.Request) { + logger.Error("[HTTP:Forbidden] %s", r.URL) - if err != nil { - w.Write(encodeError(err)) - } + builder := response.New(w, r) + builder.WithStatus(http.StatusForbidden) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.WithBody(toJSONError(errors.New("Access Forbidden"))) + builder.Write() } -func commonHeaders(w http.ResponseWriter) { - w.Header().Set("Accept", "application/json") - w.Header().Set("Content-Type", "application/json; charset=utf-8") +// NotFound sends a page not found error to the client. +func NotFound(w http.ResponseWriter, r *http.Request) { + logger.Error("[HTTP:Not Found] %s", r.URL) + + builder := response.New(w, r) + builder.WithStatus(http.StatusNotFound) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.WithBody(toJSONError(errors.New("Resource Not Found"))) + builder.Write() } -func encodeError(err error) []byte { +func toJSONError(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 + return toJSON(errorMsg{ErrorMessage: err.Error()}) } func toJSON(v interface{}) []byte { b, err := json.Marshal(v) if err != nil { - logger.Error("json encoding error: %v", err) + logger.Error("[HTTP:JSON] %v", err) return []byte("") } diff --git a/http/response/json/json_test.go b/http/response/json/json_test.go new file mode 100644 index 0000000..d22e468 --- /dev/null +++ b/http/response/json/json_test.go @@ -0,0 +1,313 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +package json // import "miniflux.app/http/response/json" + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" +) + +func TestOKResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + OK(w, r, map[string]string{"key": "value"}) + }) + + handler.ServeHTTP(w, r) + + resp := w.Result() + defer resp.Body.Close() + + expectedStatusCode := http.StatusOK + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `{"key":"value"}` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %q instead of %q`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestCreatedResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Created(w, r, map[string]string{"key": "value"}) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusCreated + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `{"key":"value"}` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestNoContentResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + NoContent(w, r) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusNoContent + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestServerErrorResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ServerError(w, r, errors.New("some error")) + }) + + handler.ServeHTTP(w, r) + + resp := w.Result() + defer resp.Body.Close() + + expectedStatusCode := http.StatusInternalServerError + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `{"error_message":"some error"}` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %q instead of %q`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestBadRequestResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + BadRequest(w, r, errors.New("Some Error")) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusBadRequest + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `{"error_message":"Some Error"}` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestUnauthorizedResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Unauthorized(w, r) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusUnauthorized + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `{"error_message":"Access Unauthorized"}` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestForbiddenResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Forbidden(w, r) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusForbidden + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `{"error_message":"Access Forbidden"}` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestNotFoundResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + NotFound(w, r) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusNotFound + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `{"error_message":"Resource Not Found"}` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestBuildInvalidJSONResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + OK(w, r, make(chan int)) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusOK + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} diff --git a/http/response/response.go b/http/response/response.go deleted file mode 100644 index 4d73171..0000000 --- a/http/response/response.go +++ /dev/null @@ -1,63 +0,0 @@ -// 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 "miniflux.app/http/response" - -import ( - "compress/flate" - "compress/gzip" - "net/http" - "strings" - "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, data []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) - return - } - - switch mimeType { - case "text/javascript; charset=utf-8", "text/css; charset=utf-8": - Compress(w, r, data) - default: - w.Write(data) - } -} - -// Compress the response sent to the browser. -func Compress(w http.ResponseWriter, r *http.Request, data []byte) { - acceptEncoding := r.Header.Get("Accept-Encoding") - - switch { - case strings.Contains(acceptEncoding, "gzip"): - w.Header().Set("Content-Encoding", "gzip") - gzipWriter := gzip.NewWriter(w) - defer gzipWriter.Close() - gzipWriter.Write(data) - case strings.Contains(acceptEncoding, "deflate"): - w.Header().Set("Content-Encoding", "deflate") - flateWriter, _ := flate.NewWriter(w, -1) - defer flateWriter.Close() - flateWriter.Write(data) - default: - w.Write(data) - } -} diff --git a/http/response/xml/doc.go b/http/response/xml/doc.go new file mode 100644 index 0000000..908d2f9 --- /dev/null +++ b/http/response/xml/doc.go @@ -0,0 +1,10 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +/* + +Package xml contains XML response functions. + +*/ +package xml // import "miniflux.app/http/response/xml" diff --git a/http/response/xml/xml.go b/http/response/xml/xml.go index bceb5f1..771a85e 100644 --- a/http/response/xml/xml.go +++ b/http/response/xml/xml.go @@ -5,19 +5,24 @@ package xml // import "miniflux.app/http/response/xml" import ( - "fmt" "net/http" + + "miniflux.app/http/response" ) -// OK sends a XML document. -func OK(w http.ResponseWriter, data string) { - w.Header().Set("Content-Type", "text/xml") - w.Write([]byte(data)) +// OK writes a standard XML response with a status 200 OK. +func OK(w http.ResponseWriter, r *http.Request, body interface{}) { + builder := response.New(w, r) + builder.WithHeader("Content-Type", "text/xml; charset=utf-8") + builder.WithBody(body) + builder.Write() } -// 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)) +// Attachment forces the XML document to be downloaded by the web browser. +func Attachment(w http.ResponseWriter, r *http.Request, filename string, body interface{}) { + builder := response.New(w, r) + builder.WithHeader("Content-Type", "text/xml; charset=utf-8") + builder.WithAttachment(filename) + builder.WithBody(body) + builder.Write() } diff --git a/http/response/xml/xml_test.go b/http/response/xml/xml_test.go new file mode 100644 index 0000000..ada6bd4 --- /dev/null +++ b/http/response/xml/xml_test.go @@ -0,0 +1,83 @@ +// 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 "miniflux.app/http/response/xml" + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestOKResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + OK(w, r, "Some XML") + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusOK + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `Some XML` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "text/xml; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestAttachmentResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Attachment(w, r, "file.xml", "Some XML") + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusOK + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `Some XML` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + headers := map[string]string{ + "Content-Type": "text/xml; charset=utf-8", + "Content-Disposition": "attachment; filename=file.xml", + } + + for header, expected := range headers { + actual := resp.Header.Get(header) + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } + } +} diff --git a/locale/translations.go b/locale/translations.go index 0c9461f..cb07083 100755 --- a/locale/translations.go +++ b/locale/translations.go @@ -478,7 +478,7 @@ var translations = map[string]string{ "error.fields_mandatory": "All fields are mandatory.", "error.title_required": "The title is mandatory.", "error.different_passwords": "Passwords are not the same.", - "error.password_min_length": "You must use at least 6 characters.", + "error.password_min_length": "The password must have at least 6 characters.", "error.settings_mandatory_fields": "The username, theme, language and timezone fields are mandatory.", "error.feed_mandatory_fields": "The URL and the category are mandatory.", "error.user_mandatory_fields": "The username is mandatory.", @@ -746,7 +746,7 @@ var translations = map[string]string{ "error.fields_mandatory": "Tous les champs sont obligatoire.", "error.title_required": "Le titre est obligatoire.", "error.different_passwords": "Les mots de passe ne sont pas les mêmes.", - "error.password_min_length": "Vous devez utiliser au moins 6 caractères.", + "error.password_min_length": "Vous devez utiliser au moins 6 caractères pour le mot de passe.", "error.settings_mandatory_fields": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.", "error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.", "error.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.", @@ -1985,8 +1985,8 @@ var translations = map[string]string{ var translationsChecksums = map[string]string{ "de_DE": "604b7a957e7633da81585553d17ef401326914e2b6605cfe50450b8f4df44310", - "en_US": "d979bd262b8631bfde29da6b22f786184f27589b1c7d7760f80d3db198868baf", - "fr_FR": "452cd05dfe9b9b797af6037798ae1d0335622e378aa8d97427503040cb5db326", + "en_US": "7dcc212a35180ba1800d5dcb5c8455e746a5eb0c3c5c79b3b31ca0eb8dc46356", + "fr_FR": "e6ebd22a6c75cebf666e18424d489db254f0b34dc15a7002a574929179efb09a", "nl_NL": "3bf3fd429bdf3e46a0be7f7e89eb06b8272a1833a04aca47ed8dd959fcac13a3", "pl_PL": "6fcf2c429ad68cb99e357825e76bc8f79d9ca3b5d01217beed5e502df2eaa5c9", "ru_RU": "5cd9093807f75f27580a1a20f0b4e4b1a4f2dc3c60b93d4fd53b5b01ecd34d71", diff --git a/locale/translations/en_US.json b/locale/translations/en_US.json index 4b8cc36..5d92a8b 100644 --- a/locale/translations/en_US.json +++ b/locale/translations/en_US.json @@ -185,7 +185,7 @@ "error.fields_mandatory": "All fields are mandatory.", "error.title_required": "The title is mandatory.", "error.different_passwords": "Passwords are not the same.", - "error.password_min_length": "You must use at least 6 characters.", + "error.password_min_length": "The password must have at least 6 characters.", "error.settings_mandatory_fields": "The username, theme, language and timezone fields are mandatory.", "error.feed_mandatory_fields": "The URL and the category are mandatory.", "error.user_mandatory_fields": "The username is mandatory.", diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json index 396071a..4145185e 100644 --- a/locale/translations/fr_FR.json +++ b/locale/translations/fr_FR.json @@ -185,7 +185,7 @@ "error.fields_mandatory": "Tous les champs sont obligatoire.", "error.title_required": "Le titre est obligatoire.", "error.different_passwords": "Les mots de passe ne sont pas les mêmes.", - "error.password_min_length": "Vous devez utiliser au moins 6 caractères.", + "error.password_min_length": "Vous devez utiliser au moins 6 caractères pour le mot de passe.", "error.settings_mandatory_fields": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.", "error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.", "error.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.", diff --git a/middleware/app_session.go b/middleware/app_session.go index b505ee1..1134e39 100644 --- a/middleware/app_session.go +++ b/middleware/app_session.go @@ -27,8 +27,7 @@ func (m *Middleware) AppSession(next http.Handler) http.Handler { session, err = m.store.CreateSession() if err != nil { - logger.Error("[Middleware:AppSession] %v", err) - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -43,7 +42,7 @@ func (m *Middleware) AppSession(next http.Handler) http.Handler { if session.Data.CSRF != formValue && session.Data.CSRF != headerValue { logger.Error(`[Middleware:AppSession] Invalid or missing CSRF token: Form="%s", Header="%s"`, formValue, headerValue) - html.BadRequest(w, errors.New("invalid or missing CSRF")) + html.BadRequest(w, r, errors.New("Invalid or missing CSRF")) return } } diff --git a/middleware/basic_auth.go b/middleware/basic_auth.go index 5a7204c..c897beb 100644 --- a/middleware/basic_auth.go +++ b/middleware/basic_auth.go @@ -22,26 +22,26 @@ func (m *Middleware) BasicAuth(next http.Handler) http.Handler { username, password, authOK := r.BasicAuth() if !authOK { logger.Debug("[Middleware:BasicAuth] No authentication headers sent") - json.Unauthorized(w) + json.Unauthorized(w, r) return } if err := m.store.CheckPassword(username, password); err != nil { logger.Error("[Middleware:BasicAuth] [ClientIP=%s] Invalid username or password: %s", clientIP, username) - json.Unauthorized(w) + json.Unauthorized(w, r) return } user, err := m.store.UserByUsername(username) if err != nil { logger.Error("[Middleware:BasicAuth] %v", err) - json.ServerError(w, err) + json.ServerError(w, r, err) return } if user == nil { logger.Error("[Middleware:BasicAuth] [ClientIP=%s] User not found: %s", clientIP, username) - json.Unauthorized(w) + json.Unauthorized(w, r) return } diff --git a/middleware/common_headers.go b/middleware/common_headers.go deleted file mode 100644 index a60969f..0000000 --- a/middleware/common_headers.go +++ /dev/null @@ -1,25 +0,0 @@ -// 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 "miniflux.app/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 9addf7e..e4dc1d4 100644 --- a/middleware/fever.go +++ b/middleware/fever.go @@ -26,7 +26,7 @@ func (m *Middleware) FeverAuth(next http.Handler) http.Handler { } if user == nil { - logger.Info("[Middleware:Fever] Fever authentication failure") + logger.Info("[Middleware:Fever] No user found with this API key") json.OK(w, r, map[string]int{"api_version": 3, "auth": 0}) return } diff --git a/middleware/header_config.go b/middleware/header_config.go index 4302ac8..d62d76c 100644 --- a/middleware/header_config.go +++ b/middleware/header_config.go @@ -14,6 +14,11 @@ func (m *Middleware) HeaderConfig(next http.Handler) http.Handler { if r.Header.Get("X-Forwarded-Proto") == "https" { m.cfg.IsHTTPS = true } + + if m.cfg.IsHTTPS && m.cfg.HasHSTS() { + w.Header().Set("Strict-Transport-Security", "max-age=31536000") + } + next.ServeHTTP(w, r) }) } diff --git a/middleware/user_session.go b/middleware/user_session.go index bddb47b..10eeca7 100644 --- a/middleware/user_session.go +++ b/middleware/user_session.go @@ -10,7 +10,7 @@ import ( "miniflux.app/http/cookie" "miniflux.app/http/request" - "miniflux.app/http/response" + "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/logger" "miniflux.app/model" @@ -28,7 +28,7 @@ func (m *Middleware) UserSession(next http.Handler) http.Handler { if m.isPublicRoute(r) { next.ServeHTTP(w, r) } else { - response.Redirect(w, r, route.Path(m.router, "login")) + html.Redirect(w, r, route.Path(m.router, "login")) } } else { logger.Debug("[Middleware:UserSession] %s", session) diff --git a/ui/about.go b/ui/about.go index e11ab52..abc39a0 100644 --- a/ui/about.go +++ b/ui/about.go @@ -18,7 +18,7 @@ import ( func (c *Controller) About(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/bookmark_entries.go b/ui/bookmark_entries.go index 08385f6..5e6bb45 100644 --- a/ui/bookmark_entries.go +++ b/ui/bookmark_entries.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -19,7 +19,7 @@ import ( func (c *Controller) ShowStarredPage(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -34,13 +34,13 @@ func (c *Controller) ShowStarredPage(w http.ResponseWriter, r *http.Request) { entries, err := builder.GetEntries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } count, err := builder.CountEntries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/category_create.go b/ui/category_create.go index 20d280a..bd5039f 100644 --- a/ui/category_create.go +++ b/ui/category_create.go @@ -17,7 +17,7 @@ import ( func (c *Controller) CreateCategory(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/category_edit.go b/ui/category_edit.go index b99d2a0..e0375be 100644 --- a/ui/category_edit.go +++ b/ui/category_edit.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -21,19 +21,19 @@ func (c *Controller) EditCategory(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } categoryID := request.RouteInt64Param(r, "categoryID") category, err := c.store.Category(request.UserID(r), categoryID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if category == nil { - html.NotFound(w) + html.NotFound(w, r) return } diff --git a/ui/category_entries.go b/ui/category_entries.go index caa98cd..fe5f638 100644 --- a/ui/category_entries.go +++ b/ui/category_entries.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -19,19 +19,19 @@ import ( func (c *Controller) CategoryEntries(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } categoryID := request.RouteInt64Param(r, "categoryID") category, err := c.store.Category(request.UserID(r), categoryID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if category == nil { - html.NotFound(w) + html.NotFound(w, r) return } @@ -46,13 +46,13 @@ func (c *Controller) CategoryEntries(w http.ResponseWriter, r *http.Request) { entries, err := builder.GetEntries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } count, err := builder.CountEntries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/category_list.go b/ui/category_list.go index 3dc17ca..f4a3651 100644 --- a/ui/category_list.go +++ b/ui/category_list.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -17,13 +17,13 @@ import ( func (c *Controller) CategoryList(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } categories, err := c.store.CategoriesWithFeedCount(user.ID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/category_remove.go b/ui/category_remove.go index b424af5..f8a38dc 100644 --- a/ui/category_remove.go +++ b/ui/category_remove.go @@ -8,7 +8,6 @@ import ( "net/http" "miniflux.app/http/request" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/route" ) @@ -17,26 +16,26 @@ import ( func (c *Controller) RemoveCategory(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } categoryID := request.RouteInt64Param(r, "categoryID") category, err := c.store.Category(request.UserID(r), categoryID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if category == nil { - html.NotFound(w) + html.NotFound(w, r) return } if err := c.store.RemoveCategory(user.ID, category.ID); err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } - response.Redirect(w, r, route.Path(c.router, "categories")) + html.Redirect(w, r, route.Path(c.router, "categories")) } diff --git a/ui/category_save.go b/ui/category_save.go index ec85cd3..d115d0a 100644 --- a/ui/category_save.go +++ b/ui/category_save.go @@ -2,12 +2,11 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/http/request" @@ -22,7 +21,7 @@ import ( func (c *Controller) SaveCategory(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -44,7 +43,7 @@ func (c *Controller) SaveCategory(w http.ResponseWriter, r *http.Request) { duplicateCategory, err := c.store.CategoryByTitle(user.ID, categoryForm.Title) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -66,5 +65,5 @@ func (c *Controller) SaveCategory(w http.ResponseWriter, r *http.Request) { return } - response.Redirect(w, r, route.Path(c.router, "categories")) + html.Redirect(w, r, route.Path(c.router, "categories")) } diff --git a/ui/category_update.go b/ui/category_update.go index 90672c0..6ef5648 100644 --- a/ui/category_update.go +++ b/ui/category_update.go @@ -2,13 +2,12 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" "miniflux.app/http/request" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/logger" @@ -21,19 +20,19 @@ import ( func (c *Controller) UpdateCategory(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } categoryID := request.RouteInt64Param(r, "categoryID") category, err := c.store.Category(request.UserID(r), categoryID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if category == nil { - html.NotFound(w) + html.NotFound(w, r) return } @@ -68,5 +67,5 @@ func (c *Controller) UpdateCategory(w http.ResponseWriter, r *http.Request) { return } - response.Redirect(w, r, route.Path(c.router, "categories")) + html.Redirect(w, r, route.Path(c.router, "categories")) } diff --git a/ui/entry_bookmark.go b/ui/entry_bookmark.go index 7c42a5c..895c2bc 100644 --- a/ui/entry_bookmark.go +++ b/ui/entry_bookmark.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -10,7 +10,6 @@ import ( "miniflux.app/http/request" "miniflux.app/http/response/html" "miniflux.app/http/route" - "miniflux.app/logger" "miniflux.app/model" "miniflux.app/storage" "miniflux.app/ui/session" @@ -21,7 +20,7 @@ import ( func (c *Controller) ShowStarredEntry(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -32,20 +31,19 @@ func (c *Controller) ShowStarredEntry(w http.ResponseWriter, r *http.Request) { entry, err := builder.GetEntry() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if entry == nil { - html.NotFound(w) + html.NotFound(w, r) 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) + html.ServerError(w, r, err) return } @@ -56,7 +54,7 @@ func (c *Controller) ShowStarredEntry(w http.ResponseWriter, r *http.Request) { entryPaginationBuilder.WithStarred() prevEntry, nextEntry, err := entryPaginationBuilder.Entries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/entry_category.go b/ui/entry_category.go index 283f015..d11248b 100644 --- a/ui/entry_category.go +++ b/ui/entry_category.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -10,7 +10,6 @@ import ( "miniflux.app/http/request" "miniflux.app/http/response/html" "miniflux.app/http/route" - "miniflux.app/logger" "miniflux.app/model" "miniflux.app/storage" "miniflux.app/ui/session" @@ -21,7 +20,7 @@ import ( func (c *Controller) ShowCategoryEntry(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -35,20 +34,19 @@ func (c *Controller) ShowCategoryEntry(w http.ResponseWriter, r *http.Request) { entry, err := builder.GetEntry() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if entry == nil { - html.NotFound(w) + html.NotFound(w, r) 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) + html.ServerError(w, r, err) return } @@ -59,7 +57,7 @@ func (c *Controller) ShowCategoryEntry(w http.ResponseWriter, r *http.Request) { entryPaginationBuilder.WithCategoryID(categoryID) prevEntry, nextEntry, err := entryPaginationBuilder.Entries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/entry_feed.go b/ui/entry_feed.go index 86dd2c9..9c28c67 100644 --- a/ui/entry_feed.go +++ b/ui/entry_feed.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -10,7 +10,6 @@ import ( "miniflux.app/http/request" "miniflux.app/http/response/html" "miniflux.app/http/route" - "miniflux.app/logger" "miniflux.app/model" "miniflux.app/storage" "miniflux.app/ui/session" @@ -21,7 +20,7 @@ import ( func (c *Controller) ShowFeedEntry(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -35,20 +34,19 @@ func (c *Controller) ShowFeedEntry(w http.ResponseWriter, r *http.Request) { entry, err := builder.GetEntry() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if entry == nil { - html.NotFound(w) + html.NotFound(w, r) 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) + html.ServerError(w, r, err) return } @@ -59,7 +57,7 @@ func (c *Controller) ShowFeedEntry(w http.ResponseWriter, r *http.Request) { entryPaginationBuilder.WithFeedID(feedID) prevEntry, nextEntry, err := entryPaginationBuilder.Entries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/entry_read.go b/ui/entry_read.go index eeaca8e..208ae3b 100644 --- a/ui/entry_read.go +++ b/ui/entry_read.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -20,7 +20,7 @@ import ( func (c *Controller) ShowReadEntry(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -31,12 +31,12 @@ func (c *Controller) ShowReadEntry(w http.ResponseWriter, r *http.Request) { entry, err := builder.GetEntry() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if entry == nil { - html.NotFound(w) + html.NotFound(w, r) return } @@ -44,7 +44,7 @@ func (c *Controller) ShowReadEntry(w http.ResponseWriter, r *http.Request) { entryPaginationBuilder.WithStatus(model.EntryStatusRead) prevEntry, nextEntry, err := entryPaginationBuilder.Entries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/entry_save.go b/ui/entry_save.go index 1f846ba..93910c0 100644 --- a/ui/entry_save.go +++ b/ui/entry_save.go @@ -5,7 +5,6 @@ package ui // import "miniflux.app/ui" import ( - "errors" "net/http" "miniflux.app/http/request" @@ -23,18 +22,18 @@ func (c *Controller) SaveEntry(w http.ResponseWriter, r *http.Request) { entry, err := builder.GetEntry() if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } if entry == nil { - json.NotFound(w, errors.New("Entry not found")) + json.NotFound(w, r) return } settings, err := c.store.Integration(request.UserID(r)) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } @@ -42,5 +41,5 @@ func (c *Controller) SaveEntry(w http.ResponseWriter, r *http.Request) { integration.SendEntry(c.cfg, entry, settings) }() - json.Created(w, map[string]string{"message": "saved"}) + json.Created(w, r, map[string]string{"message": "saved"}) } diff --git a/ui/entry_scraper.go b/ui/entry_scraper.go index 4c2d58c..c35d949 100644 --- a/ui/entry_scraper.go +++ b/ui/entry_scraper.go @@ -2,10 +2,9 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( - "errors" "net/http" "miniflux.app/http/request" @@ -24,18 +23,18 @@ func (c *Controller) FetchContent(w http.ResponseWriter, r *http.Request) { entry, err := builder.GetEntry() if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } if entry == nil { - json.NotFound(w, errors.New("Entry not found")) + json.NotFound(w, r) return } content, err := scraper.Fetch(entry.URL, entry.Feed.ScraperRules, entry.Feed.UserAgent) if err != nil { - json.ServerError(w, err) + json.ServerError(w, r, err) return } diff --git a/ui/entry_search.go b/ui/entry_search.go index 8acf103..d083b35 100644 --- a/ui/entry_search.go +++ b/ui/entry_search.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -21,7 +21,7 @@ import ( func (c *Controller) ShowSearchEntry(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -34,12 +34,12 @@ func (c *Controller) ShowSearchEntry(w http.ResponseWriter, r *http.Request) { entry, err := builder.GetEntry() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if entry == nil { - html.NotFound(w) + html.NotFound(w, r) return } @@ -47,7 +47,7 @@ func (c *Controller) ShowSearchEntry(w http.ResponseWriter, r *http.Request) { err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead) if err != nil { logger.Error("[Controller:ShowSearchEntry] %v", err) - html.ServerError(w, nil) + html.ServerError(w, r, err) return } @@ -58,7 +58,7 @@ func (c *Controller) ShowSearchEntry(w http.ResponseWriter, r *http.Request) { entryPaginationBuilder.WithSearchQuery(searchQuery) prevEntry, nextEntry, err := entryPaginationBuilder.Entries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/entry_toggle_bookmark.go b/ui/entry_toggle_bookmark.go index 14a2c75..9f7a3e7 100644 --- a/ui/entry_toggle_bookmark.go +++ b/ui/entry_toggle_bookmark.go @@ -9,15 +9,13 @@ import ( "miniflux.app/http/request" "miniflux.app/http/response/json" - "miniflux.app/logger" ) // ToggleBookmark handles Ajax request to toggle bookmark value. func (c *Controller) ToggleBookmark(w http.ResponseWriter, r *http.Request) { entryID := request.RouteInt64Param(r, "entryID") if err := c.store.ToggleBookmark(request.UserID(r), entryID); err != nil { - logger.Error("[Controller:ToggleBookmark] %v", err) - json.ServerError(w, nil) + json.ServerError(w, r, err) return } diff --git a/ui/entry_unread.go b/ui/entry_unread.go index 4ef5731..37e9592 100644 --- a/ui/entry_unread.go +++ b/ui/entry_unread.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -10,7 +10,6 @@ import ( "miniflux.app/http/request" "miniflux.app/http/response/html" "miniflux.app/http/route" - "miniflux.app/logger" "miniflux.app/model" "miniflux.app/storage" "miniflux.app/ui/session" @@ -21,7 +20,7 @@ import ( func (c *Controller) ShowUnreadEntry(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -32,12 +31,12 @@ func (c *Controller) ShowUnreadEntry(w http.ResponseWriter, r *http.Request) { entry, err := builder.GetEntry() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if entry == nil { - html.NotFound(w) + html.NotFound(w, r) return } @@ -45,8 +44,7 @@ func (c *Controller) ShowUnreadEntry(w http.ResponseWriter, r *http.Request) { if entry.Status == model.EntryStatusRead { err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusUnread) if err != nil { - logger.Error("[Controller:ShowUnreadEntry] %v", err) - html.ServerError(w, nil) + html.ServerError(w, r, err) return } } @@ -55,7 +53,7 @@ func (c *Controller) ShowUnreadEntry(w http.ResponseWriter, r *http.Request) { entryPaginationBuilder.WithStatus(model.EntryStatusUnread) prevEntry, nextEntry, err := entryPaginationBuilder.Entries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -72,8 +70,7 @@ func (c *Controller) ShowUnreadEntry(w http.ResponseWriter, r *http.Request) { // Always mark the entry as read after fetching the pagination. err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead) if err != nil { - logger.Error("[Controller:ShowUnreadEntry] %v", err) - html.ServerError(w, nil) + html.ServerError(w, r, err) return } entry.Status = model.EntryStatusRead diff --git a/ui/entry_update_status.go b/ui/entry_update_status.go index 8e9de4d..6c5cb12 100644 --- a/ui/entry_update_status.go +++ b/ui/entry_update_status.go @@ -10,27 +10,24 @@ import ( "miniflux.app/http/request" "miniflux.app/http/response/json" - "miniflux.app/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) + json.BadRequest(w, r, err) return } if len(entryIDs) == 0 { - json.BadRequest(w, errors.New("The list of entryID is empty")) + json.BadRequest(w, r, errors.New("The list of entry IDs is empty")) return } err = c.store.SetEntriesStatus(request.UserID(r), entryIDs, status) if err != nil { - logger.Error("[Controller:UpdateEntryStatus] %v", err) - json.ServerError(w, nil) + json.ServerError(w, r, err) return } diff --git a/ui/feed_edit.go b/ui/feed_edit.go index 8b1b3cb..330817b 100644 --- a/ui/feed_edit.go +++ b/ui/feed_edit.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -19,25 +19,25 @@ import ( func (c *Controller) EditFeed(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } feedID := request.RouteInt64Param(r, "feedID") feed, err := c.store.FeedByID(user.ID, feedID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if feed == nil { - html.NotFound(w) + html.NotFound(w, r) return } categories, err := c.store.Categories(user.ID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/feed_entries.go b/ui/feed_entries.go index 06a298b..685b5c1 100644 --- a/ui/feed_entries.go +++ b/ui/feed_entries.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -19,19 +19,19 @@ import ( func (c *Controller) ShowFeedEntries(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } feedID := request.RouteInt64Param(r, "feedID") feed, err := c.store.FeedByID(user.ID, feedID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if feed == nil { - html.NotFound(w) + html.NotFound(w, r) return } @@ -46,13 +46,13 @@ func (c *Controller) ShowFeedEntries(w http.ResponseWriter, r *http.Request) { entries, err := builder.GetEntries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } count, err := builder.CountEntries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/feed_icon.go b/ui/feed_icon.go index 0aa7089..63aa050 100644 --- a/ui/feed_icon.go +++ b/ui/feed_icon.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -18,14 +18,19 @@ func (c *Controller) ShowIcon(w http.ResponseWriter, r *http.Request) { iconID := request.RouteInt64Param(r, "iconID") icon, err := c.store.IconByID(iconID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if icon == nil { - html.NotFound(w) + html.NotFound(w, r) return } - response.Cache(w, r, icon.MimeType, icon.Hash, icon.Content, 72*time.Hour) + response.New(w, r).WithCaching(icon.Hash, 72*time.Hour, func(b *response.Builder) { + b.WithHeader("Content-Type", icon.MimeType) + b.WithBody(icon.Content) + b.WithoutCompression() + b.Write() + }) } diff --git a/ui/feed_list.go b/ui/feed_list.go index 9fe4aac..ac5a97c 100644 --- a/ui/feed_list.go +++ b/ui/feed_list.go @@ -17,13 +17,13 @@ import ( func (c *Controller) ShowFeedsPage(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } feeds, err := c.store.Feeds(user.ID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/feed_refresh.go b/ui/feed_refresh.go index df93c6e..0da4eb8 100644 --- a/ui/feed_refresh.go +++ b/ui/feed_refresh.go @@ -2,13 +2,12 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" "miniflux.app/http/request" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/logger" @@ -21,7 +20,7 @@ func (c *Controller) RefreshFeed(w http.ResponseWriter, r *http.Request) { logger.Error("[Controller:RefreshFeed] %v", err) } - response.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feedID)) + html.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feedID)) } // RefreshAllFeeds refresh all feeds in the background for the current user. @@ -29,7 +28,7 @@ func (c *Controller) RefreshAllFeeds(w http.ResponseWriter, r *http.Request) { userID := request.UserID(r) jobs, err := c.store.NewUserBatch(userID, c.store.CountFeeds(userID)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -37,5 +36,5 @@ func (c *Controller) RefreshAllFeeds(w http.ResponseWriter, r *http.Request) { c.pool.Push(jobs) }() - response.Redirect(w, r, route.Path(c.router, "feeds")) + html.Redirect(w, r, route.Path(c.router, "feeds")) } diff --git a/ui/feed_remove.go b/ui/feed_remove.go index d1ab01a..41fc5b0 100644 --- a/ui/feed_remove.go +++ b/ui/feed_remove.go @@ -8,7 +8,6 @@ import ( "net/http" "miniflux.app/http/request" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/route" ) @@ -17,9 +16,9 @@ import ( func (c *Controller) RemoveFeed(w http.ResponseWriter, r *http.Request) { feedID := request.RouteInt64Param(r, "feedID") if err := c.store.RemoveFeed(request.UserID(r), feedID); err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } - response.Redirect(w, r, route.Path(c.router, "feeds")) + html.Redirect(w, r, route.Path(c.router, "feeds")) } diff --git a/ui/feed_update.go b/ui/feed_update.go index 6cc4776..66b6d40 100644 --- a/ui/feed_update.go +++ b/ui/feed_update.go @@ -2,14 +2,13 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" "miniflux.app/http/client" "miniflux.app/http/request" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/logger" @@ -22,25 +21,25 @@ import ( func (c *Controller) UpdateFeed(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } feedID := request.RouteInt64Param(r, "feedID") feed, err := c.store.FeedByID(user.ID, feedID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if feed == nil { - html.NotFound(w) + html.NotFound(w, r) return } categories, err := c.store.Categories(user.ID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -71,5 +70,5 @@ func (c *Controller) UpdateFeed(w http.ResponseWriter, r *http.Request) { return } - response.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID)) + html.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID)) } diff --git a/ui/history_entries.go b/ui/history_entries.go index 25310a9..8449240 100644 --- a/ui/history_entries.go +++ b/ui/history_entries.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -19,7 +19,7 @@ import ( func (c *Controller) ShowHistoryPage(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -33,13 +33,13 @@ func (c *Controller) ShowHistoryPage(w http.ResponseWriter, r *http.Request) { entries, err := builder.GetEntries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } count, err := builder.CountEntries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/history_flush.go b/ui/history_flush.go index 5bb8910..6c6907b 100644 --- a/ui/history_flush.go +++ b/ui/history_flush.go @@ -8,7 +8,6 @@ import ( "net/http" "miniflux.app/http/request" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/route" ) @@ -17,9 +16,9 @@ import ( func (c *Controller) FlushHistory(w http.ResponseWriter, r *http.Request) { err := c.store.FlushHistory(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } - response.Redirect(w, r, route.Path(c.router, "history")) + html.Redirect(w, r, route.Path(c.router, "history")) } diff --git a/ui/integration_pocket.go b/ui/integration_pocket.go index 407731e..47a975c 100644 --- a/ui/integration_pocket.go +++ b/ui/integration_pocket.go @@ -7,9 +7,8 @@ package ui // import "miniflux.app/ui" import ( "net/http" - "miniflux.app/http/response" - "miniflux.app/http/request" "miniflux.app/http/response/html" + "miniflux.app/http/request" "miniflux.app/http/route" "miniflux.app/integration/pocket" "miniflux.app/locale" @@ -22,13 +21,13 @@ func (c *Controller) PocketAuthorize(w http.ResponseWriter, r *http.Request) { printer := locale.NewPrinter(request.UserLanguage(r)) user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } integration, err := c.store.Integration(user.ID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -39,12 +38,12 @@ func (c *Controller) PocketAuthorize(w http.ResponseWriter, r *http.Request) { if err != nil { logger.Error("[Pocket:Authorize] %v", err) sess.NewFlashErrorMessage(printer.Printf("error.pocket_request_token")) - response.Redirect(w, r, route.Path(c.router, "integrations")) + html.Redirect(w, r, route.Path(c.router, "integrations")) return } sess.SetPocketRequestToken(requestToken) - response.Redirect(w, r, connector.AuthorizationURL(requestToken, redirectURL)) + html.Redirect(w, r, connector.AuthorizationURL(requestToken, redirectURL)) } // PocketCallback saves the personal access token after the authorization step. @@ -54,13 +53,13 @@ func (c *Controller) PocketCallback(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } integration, err := c.store.Integration(user.ID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -69,7 +68,7 @@ func (c *Controller) PocketCallback(w http.ResponseWriter, r *http.Request) { if err != nil { logger.Error("[Pocket:Callback] %v", err) sess.NewFlashErrorMessage(printer.Printf("error.pocket_access_token")) - response.Redirect(w, r, route.Path(c.router, "integrations")) + html.Redirect(w, r, route.Path(c.router, "integrations")) return } @@ -78,10 +77,10 @@ func (c *Controller) PocketCallback(w http.ResponseWriter, r *http.Request) { err = c.store.UpdateIntegration(integration) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } sess.NewFlashMessage(printer.Printf("alert.pocket_linked")) - response.Redirect(w, r, route.Path(c.router, "integrations")) + html.Redirect(w, r, route.Path(c.router, "integrations")) } diff --git a/ui/integration_show.go b/ui/integration_show.go index 1c8adce..3cea830 100644 --- a/ui/integration_show.go +++ b/ui/integration_show.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -18,13 +18,13 @@ import ( func (c *Controller) ShowIntegrations(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } integration, err := c.store.Integration(user.ID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/integration_update.go b/ui/integration_update.go index f470e4b..5661daf 100644 --- a/ui/integration_update.go +++ b/ui/integration_update.go @@ -2,16 +2,15 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "crypto/md5" "fmt" "net/http" - "miniflux.app/http/response" - "miniflux.app/http/request" "miniflux.app/http/response/html" + "miniflux.app/http/request" "miniflux.app/http/route" "miniflux.app/locale" "miniflux.app/ui/form" @@ -24,13 +23,13 @@ func (c *Controller) UpdateIntegration(w http.ResponseWriter, r *http.Request) { sess := session.New(c.store, request.SessionID(r)) user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } integration, err := c.store.Integration(user.ID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -39,7 +38,7 @@ func (c *Controller) UpdateIntegration(w http.ResponseWriter, r *http.Request) { if integration.FeverUsername != "" && c.store.HasDuplicateFeverUsername(user.ID, integration.FeverUsername) { sess.NewFlashErrorMessage(printer.Printf("error.duplicate_fever_username")) - response.Redirect(w, r, route.Path(c.router, "integrations")) + html.Redirect(w, r, route.Path(c.router, "integrations")) return } @@ -51,10 +50,10 @@ func (c *Controller) UpdateIntegration(w http.ResponseWriter, r *http.Request) { err = c.store.UpdateIntegration(integration) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } sess.NewFlashMessage(printer.Printf("alert.prefs_saved")) - response.Redirect(w, r, route.Path(c.router, "integrations")) + html.Redirect(w, r, route.Path(c.router, "integrations")) } diff --git a/ui/login_check.go b/ui/login_check.go index 98d2c4e..73a1736 100644 --- a/ui/login_check.go +++ b/ui/login_check.go @@ -5,7 +5,6 @@ import ( "miniflux.app/http/cookie" "miniflux.app/http/request" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/logger" @@ -38,7 +37,7 @@ func (c *Controller) CheckLogin(w http.ResponseWriter, r *http.Request) { sessionToken, userID, err := c.store.CreateUserSession(authForm.Username, r.UserAgent(), clientIP) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -47,7 +46,7 @@ func (c *Controller) CheckLogin(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(userID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -61,5 +60,5 @@ func (c *Controller) CheckLogin(w http.ResponseWriter, r *http.Request) { c.cfg.BasePath(), )) - response.Redirect(w, r, route.Path(c.router, "unread")) + html.Redirect(w, r, route.Path(c.router, "unread")) } diff --git a/ui/login_show.go b/ui/login_show.go index 82c19ec..890f9db 100644 --- a/ui/login_show.go +++ b/ui/login_show.go @@ -2,14 +2,13 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" - "miniflux.app/http/response" - "miniflux.app/http/request" "miniflux.app/http/response/html" + "miniflux.app/http/request" "miniflux.app/http/route" "miniflux.app/ui/session" "miniflux.app/ui/view" @@ -18,7 +17,7 @@ import ( // ShowLoginPage shows the login form. func (c *Controller) ShowLoginPage(w http.ResponseWriter, r *http.Request) { if request.IsAuthenticated(r) { - response.Redirect(w, r, route.Path(c.router, "unread")) + html.Redirect(w, r, route.Path(c.router, "unread")) return } diff --git a/ui/logout.go b/ui/logout.go index 0c8e9e7..59f8af2 100644 --- a/ui/logout.go +++ b/ui/logout.go @@ -2,14 +2,13 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" "miniflux.app/http/cookie" "miniflux.app/http/request" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/logger" @@ -21,7 +20,7 @@ func (c *Controller) Logout(w http.ResponseWriter, r *http.Request) { sess := session.New(c.store, request.SessionID(r)) user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -38,5 +37,5 @@ func (c *Controller) Logout(w http.ResponseWriter, r *http.Request) { c.cfg.BasePath(), )) - response.Redirect(w, r, route.Path(c.router, "login")) + html.Redirect(w, r, route.Path(c.router, "login")) } diff --git a/ui/oauth2.go b/ui/oauth2.go index 3f964a6..c5d594f 100644 --- a/ui/oauth2.go +++ b/ui/oauth2.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "miniflux.app/config" diff --git a/ui/oauth2_callback.go b/ui/oauth2_callback.go index 1902d6e..feaccc4 100644 --- a/ui/oauth2_callback.go +++ b/ui/oauth2_callback.go @@ -2,14 +2,13 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" "miniflux.app/http/cookie" "miniflux.app/http/request" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/locale" @@ -26,71 +25,71 @@ func (c *Controller) OAuth2Callback(w http.ResponseWriter, r *http.Request) { provider := request.RouteStringParam(r, "provider") if provider == "" { logger.Error("[OAuth2] Invalid or missing provider") - response.Redirect(w, r, route.Path(c.router, "login")) + html.Redirect(w, r, route.Path(c.router, "login")) return } code := request.QueryStringParam(r, "code", "") if code == "" { logger.Error("[OAuth2] No code received on callback") - response.Redirect(w, r, route.Path(c.router, "login")) + html.Redirect(w, r, route.Path(c.router, "login")) return } state := request.QueryStringParam(r, "state", "") if state == "" || state != request.OAuth2State(r) { logger.Error(`[OAuth2] Invalid state value: got "%s" instead of "%s"`, state, request.OAuth2State(r)) - response.Redirect(w, r, route.Path(c.router, "login")) + html.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")) + html.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")) + html.Redirect(w, r, route.Path(c.router, "login")) return } if request.IsAuthenticated(r) { user, err := c.store.UserByExtraField(profile.Key, profile.ID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if user != nil { logger.Error("[OAuth2] User #%d cannot be associated because %s is already associated", request.UserID(r), user.Username) sess.NewFlashErrorMessage(printer.Printf("error.duplicate_linked_account")) - response.Redirect(w, r, route.Path(c.router, "settings")) + html.Redirect(w, r, route.Path(c.router, "settings")) return } if err := c.store.UpdateExtraField(request.UserID(r), profile.Key, profile.ID); err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } sess.NewFlashMessage(printer.Printf("alert.account_linked")) - response.Redirect(w, r, route.Path(c.router, "settings")) + html.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) + html.ServerError(w, r, err) return } if user == nil { if !c.cfg.IsOAuth2UserCreationAllowed() { - html.Forbidden(w) + html.Forbidden(w, r) return } @@ -100,14 +99,14 @@ func (c *Controller) OAuth2Callback(w http.ResponseWriter, r *http.Request) { user.Extra[profile.Key] = profile.ID if err := c.store.CreateUser(user); err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } } sessionToken, _, err := c.store.CreateUserSession(user.Username, r.UserAgent(), request.ClientIP(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -123,5 +122,5 @@ func (c *Controller) OAuth2Callback(w http.ResponseWriter, r *http.Request) { c.cfg.BasePath(), )) - response.Redirect(w, r, route.Path(c.router, "unread")) + html.Redirect(w, r, route.Path(c.router, "unread")) } diff --git a/ui/oauth2_redirect.go b/ui/oauth2_redirect.go index 90c20b1..e54309a 100644 --- a/ui/oauth2_redirect.go +++ b/ui/oauth2_redirect.go @@ -2,13 +2,13 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" "miniflux.app/http/request" - "miniflux.app/http/response" + "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/logger" "miniflux.app/ui/session" @@ -21,16 +21,16 @@ func (c *Controller) OAuth2Redirect(w http.ResponseWriter, r *http.Request) { provider := request.RouteStringParam(r, "provider") if provider == "" { logger.Error("[OAuth2] Invalid or missing provider: %s", provider) - response.Redirect(w, r, route.Path(c.router, "login")) + html.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")) + html.Redirect(w, r, route.Path(c.router, "login")) return } - response.Redirect(w, r, authProvider.GetRedirectURL(sess.NewOAuth2State())) + html.Redirect(w, r, authProvider.GetRedirectURL(sess.NewOAuth2State())) } diff --git a/ui/oauth2_unlink.go b/ui/oauth2_unlink.go index 022e282..8e38ddc 100644 --- a/ui/oauth2_unlink.go +++ b/ui/oauth2_unlink.go @@ -2,13 +2,12 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" "miniflux.app/http/request" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/locale" @@ -22,14 +21,14 @@ func (c *Controller) OAuth2Unlink(w http.ResponseWriter, r *http.Request) { provider := request.RouteStringParam(r, "provider") if provider == "" { logger.Info("[OAuth2] Invalid or missing provider") - response.Redirect(w, r, route.Path(c.router, "login")) + html.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")) + html.Redirect(w, r, route.Path(c.router, "settings")) return } @@ -37,21 +36,21 @@ func (c *Controller) OAuth2Unlink(w http.ResponseWriter, r *http.Request) { hasPassword, err := c.store.HasPassword(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if !hasPassword { sess.NewFlashErrorMessage(printer.Printf("error.unlink_account_without_password")) - response.Redirect(w, r, route.Path(c.router, "settings")) + html.Redirect(w, r, route.Path(c.router, "settings")) return } if err := c.store.RemoveExtraField(request.UserID(r), authProvider.GetUserExtraKey()); err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } sess.NewFlashMessage(printer.Printf("alert.account_unlinked")) - response.Redirect(w, r, route.Path(c.router, "settings")) + html.Redirect(w, r, route.Path(c.router, "settings")) } diff --git a/ui/opml_export.go b/ui/opml_export.go index 20194b0..55ceb56 100644 --- a/ui/opml_export.go +++ b/ui/opml_export.go @@ -17,9 +17,9 @@ import ( func (c *Controller) Export(w http.ResponseWriter, r *http.Request) { opml, err := opml.NewHandler(c.store).Export(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } - xml.Attachment(w, "feeds.opml", opml) + xml.Attachment(w, r, "feeds.opml", opml) } diff --git a/ui/opml_import.go b/ui/opml_import.go index a8e732e..9a763ca 100644 --- a/ui/opml_import.go +++ b/ui/opml_import.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -17,7 +17,7 @@ import ( func (c *Controller) Import(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/opml_upload.go b/ui/opml_upload.go index 603b660..538f822 100644 --- a/ui/opml_upload.go +++ b/ui/opml_upload.go @@ -2,13 +2,12 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" "miniflux.app/http/request" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/logger" @@ -21,14 +20,14 @@ import ( func (c *Controller) UploadOPML(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, 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")) + html.Redirect(w, r, route.Path(c.router, "import")) return } defer file.Close() @@ -59,5 +58,5 @@ func (c *Controller) UploadOPML(w http.ResponseWriter, r *http.Request) { return } - response.Redirect(w, r, route.Path(c.router, "feeds")) + html.Redirect(w, r, route.Path(c.router, "feeds")) } diff --git a/ui/proxy.go b/ui/proxy.go index 553dcce..050c070 100644 --- a/ui/proxy.go +++ b/ui/proxy.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "encoding/base64" @@ -16,44 +16,47 @@ import ( "miniflux.app/http/request" "miniflux.app/http/response" "miniflux.app/http/response/html" - "miniflux.app/logger" ) // ImageProxy fetch an image from a remote server and sent it back to the browser. func (c *Controller) ImageProxy(w http.ResponseWriter, r *http.Request) { - // If we receive a "If-None-Match" header we assume the image in stored in browser cache + // If we receive a "If-None-Match" header, we assume the image is already stored in browser cache. if r.Header.Get("If-None-Match") != "" { - response.NotModified(w) + w.WriteHeader(http.StatusNotModified) return } encodedURL := request.RouteStringParam(r, "encodedURL") if encodedURL == "" { - html.BadRequest(w, errors.New("No URL provided")) + html.BadRequest(w, r, errors.New("No URL provided")) return } decodedURL, err := base64.URLEncoding.DecodeString(encodedURL) if err != nil { - html.BadRequest(w, errors.New("Unable to decode this URL")) + html.BadRequest(w, r, errors.New("Unable to decode this URL")) return } clt := client.New(string(decodedURL)) resp, err := clt.Get() if err != nil { - logger.Error("[Controller:ImageProxy] %v", err) - html.NotFound(w) + html.ServerError(w, r, err) return } if resp.HasServerFailure() { - html.NotFound(w) + html.NotFound(w, r) return } body, _ := ioutil.ReadAll(resp.Body) etag := crypto.HashFromBytes(body) - response.Cache(w, r, resp.ContentType, etag, body, 72*time.Hour) + response.New(w ,r).WithCaching(etag, 72*time.Hour, func(b *response.Builder) { + b.WithHeader("Content-Type", resp.ContentType) + b.WithBody(body) + b.WithoutCompression() + b.Write() + }) } diff --git a/ui/search_entries.go b/ui/search_entries.go index e1dcbad..1304ef2 100644 --- a/ui/search_entries.go +++ b/ui/search_entries.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -19,7 +19,7 @@ import ( func (c *Controller) ShowSearchEntries(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -35,13 +35,13 @@ func (c *Controller) ShowSearchEntries(w http.ResponseWriter, r *http.Request) { entries, err := builder.GetEntries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } count, err := builder.CountEntries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/session_list.go b/ui/session_list.go index 3f5bf9e..76e693a 100644 --- a/ui/session_list.go +++ b/ui/session_list.go @@ -2,15 +2,15 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" "miniflux.app/http/request" + "miniflux.app/http/response/html" "miniflux.app/ui/session" "miniflux.app/ui/view" - "miniflux.app/http/response/html" ) // ShowSessions shows the list of active user sessions. @@ -20,13 +20,13 @@ func (c *Controller) ShowSessions(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } sessions, err := c.store.UserSessions(user.ID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/session_remove.go b/ui/session_remove.go index 27201fd..cc62612 100644 --- a/ui/session_remove.go +++ b/ui/session_remove.go @@ -8,7 +8,7 @@ import ( "net/http" "miniflux.app/http/request" - "miniflux.app/http/response" + "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/logger" ) @@ -21,5 +21,5 @@ func (c *Controller) RemoveSession(w http.ResponseWriter, r *http.Request) { logger.Error("[Controller:RemoveSession] %v", err) } - response.Redirect(w, r, route.Path(c.router, "sessions")) + html.Redirect(w, r, route.Path(c.router, "sessions")) } diff --git a/ui/settings_show.go b/ui/settings_show.go index 07703eb..4f4756d 100644 --- a/ui/settings_show.go +++ b/ui/settings_show.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -23,7 +23,7 @@ func (c *Controller) ShowSettings(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -37,7 +37,7 @@ func (c *Controller) ShowSettings(w http.ResponseWriter, r *http.Request) { timezones, err := c.store.Timezones() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/settings_update.go b/ui/settings_update.go index ed424e3..466bb78 100644 --- a/ui/settings_update.go +++ b/ui/settings_update.go @@ -2,12 +2,11 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/request" "miniflux.app/http/route" @@ -26,13 +25,13 @@ func (c *Controller) UpdateSettings(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } timezones, err := c.store.Timezones() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -70,5 +69,5 @@ func (c *Controller) UpdateSettings(w http.ResponseWriter, r *http.Request) { sess.SetLanguage(user.Language) sess.SetTheme(user.Theme) sess.NewFlashMessage(locale.NewPrinter(request.UserLanguage(r)).Printf("alert.prefs_saved")) - response.Redirect(w, r, route.Path(c.router, "settings")) + html.Redirect(w, r, route.Path(c.router, "settings")) } diff --git a/ui/static_app_icon.go b/ui/static_app_icon.go index 2ad42fd..9e2a448 100644 --- a/ui/static_app_icon.go +++ b/ui/static_app_icon.go @@ -12,26 +12,28 @@ import ( "miniflux.app/http/request" "miniflux.app/http/response" "miniflux.app/http/response/html" - "miniflux.app/logger" "miniflux.app/ui/static" ) -// AppIcon renders application icons. +// AppIcon shows application icons. func (c *Controller) AppIcon(w http.ResponseWriter, r *http.Request) { filename := request.RouteStringParam(r, "filename") - encodedBlob, found := static.Binaries[filename] + etag, found := static.BinariesChecksums[filename] if !found { - logger.Info("[Controller:AppIcon] This icon doesn't exists: %s", filename) - html.NotFound(w) + html.NotFound(w, r) return } - blob, err := base64.StdEncoding.DecodeString(encodedBlob) - if err != nil { - logger.Error("[Controller:AppIcon] %v", err) - html.NotFound(w) - return - } + response.New(w, r).WithCaching(etag, 72*time.Hour, func(b *response.Builder) { + blob, err := base64.StdEncoding.DecodeString(static.Binaries[filename]) + if err != nil { + html.ServerError(w, r, err) + return + } - response.Cache(w, r, "image/png", static.BinariesChecksums[filename], blob, 48*time.Hour) + b.WithHeader("Content-Type", "image/png") + b.WithoutCompression() + b.WithBody(blob) + b.Write() + }) } diff --git a/ui/static_favicon.go b/ui/static_favicon.go index 2e19c05..1266a19 100644 --- a/ui/static_favicon.go +++ b/ui/static_favicon.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "encoding/base64" @@ -11,18 +11,27 @@ import ( "miniflux.app/http/response" "miniflux.app/http/response/html" - "miniflux.app/logger" "miniflux.app/ui/static" ) -// Favicon renders the application favicon. +// Favicon shows 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) + etag, found := static.BinariesChecksums["favicon.ico"] + if !found { + html.NotFound(w, r) return } - response.Cache(w, r, "image/x-icon", static.BinariesChecksums["favicon.ico"], blob, 48*time.Hour) + response.New(w, r).WithCaching(etag, 48*time.Hour, func(b *response.Builder) { + blob, err := base64.StdEncoding.DecodeString(static.Binaries["favicon.ico"]) + if err != nil { + html.ServerError(w, r, err) + return + } + + b.WithHeader("Content-Type", "image/x-icon") + b.WithoutCompression() + b.WithBody(blob) + b.Write() + }) } diff --git a/ui/static_javascript.go b/ui/static_javascript.go index 248fae3..ff7fd16 100644 --- a/ui/static_javascript.go +++ b/ui/static_javascript.go @@ -17,13 +17,15 @@ import ( // Javascript renders application client side code. func (c *Controller) Javascript(w http.ResponseWriter, r *http.Request) { filename := request.RouteStringParam(r, "name") - if _, found := static.Javascripts[filename]; !found { - html.NotFound(w) + etag, found := static.JavascriptsChecksums[filename] + if !found { + html.NotFound(w, r) return } - body := static.Javascripts[filename] - etag := static.JavascriptsChecksums[filename] - - response.Cache(w, r, "text/javascript; charset=utf-8", etag, []byte(body), 48*time.Hour) + response.New(w, r).WithCaching(etag, 48*time.Hour, func(b *response.Builder) { + b.WithHeader("Content-Type", "text/javascript; charset=utf-8") + b.WithBody(static.Javascripts[filename]) + b.Write() + }) } diff --git a/ui/static_stylesheet.go b/ui/static_stylesheet.go index 8e475bd..9a21bc6 100644 --- a/ui/static_stylesheet.go +++ b/ui/static_stylesheet.go @@ -16,14 +16,16 @@ import ( // Stylesheet renders the CSS. func (c *Controller) Stylesheet(w http.ResponseWriter, r *http.Request) { - stylesheet := request.RouteStringParam(r, "name") - if _, found := static.Stylesheets[stylesheet]; !found { - html.NotFound(w) + filename := request.RouteStringParam(r, "name") + etag, found := static.StylesheetsChecksums[filename] + if !found { + html.NotFound(w, r) return } - body := static.Stylesheets[stylesheet] - etag := static.StylesheetsChecksums[stylesheet] - - response.Cache(w, r, "text/css; charset=utf-8", etag, []byte(body), 48*time.Hour) + response.New(w, r).WithCaching(etag, 48*time.Hour, func(b *response.Builder) { + b.WithHeader("Content-Type", "text/css; charset=utf-8") + b.WithBody(static.Stylesheets[filename]) + b.Write() + }) } diff --git a/ui/subscription_add.go b/ui/subscription_add.go index 387f340..3056eec 100644 --- a/ui/subscription_add.go +++ b/ui/subscription_add.go @@ -21,13 +21,13 @@ func (c *Controller) AddSubscription(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } categories, err := c.store.Categories(user.ID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/subscription_bookmarklet.go b/ui/subscription_bookmarklet.go index cba039a..5c0e08c 100644 --- a/ui/subscription_bookmarklet.go +++ b/ui/subscription_bookmarklet.go @@ -22,13 +22,13 @@ func (c *Controller) Bookmarklet(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } categories, err := c.store.Categories(user.ID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/subscription_choose.go b/ui/subscription_choose.go index 10e46ad..4f701c8 100644 --- a/ui/subscription_choose.go +++ b/ui/subscription_choose.go @@ -2,13 +2,12 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" "miniflux.app/http/client" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/request" "miniflux.app/http/route" @@ -24,13 +23,13 @@ func (c *Controller) ChooseSubscription(w http.ResponseWriter, r *http.Request) user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } categories, err := c.store.Categories(user.ID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -65,5 +64,5 @@ func (c *Controller) ChooseSubscription(w http.ResponseWriter, r *http.Request) return } - response.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID)) + html.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID)) } diff --git a/ui/subscription_submit.go b/ui/subscription_submit.go index d1cefbd..801e516 100644 --- a/ui/subscription_submit.go +++ b/ui/subscription_submit.go @@ -8,7 +8,6 @@ import ( "net/http" "miniflux.app/http/client" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/request" "miniflux.app/http/route" @@ -26,13 +25,13 @@ func (c *Controller) SubmitSubscription(w http.ResponseWriter, r *http.Request) user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } categories, err := c.store.Categories(user.ID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -90,7 +89,7 @@ func (c *Controller) SubmitSubscription(w http.ResponseWriter, r *http.Request) return } - response.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID)) + html.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID)) case n > 1: v := view.New(c.tpl, r, sess) v.Set("subscriptions", subscriptions) diff --git a/ui/unread_entries.go b/ui/unread_entries.go index 5992162..cb02428 100644 --- a/ui/unread_entries.go +++ b/ui/unread_entries.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -22,7 +22,7 @@ func (c *Controller) ShowUnreadPage(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -31,7 +31,7 @@ func (c *Controller) ShowUnreadPage(w http.ResponseWriter, r *http.Request) { builder.WithStatus(model.EntryStatusUnread) countUnread, err := builder.CountEntries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } @@ -47,7 +47,7 @@ func (c *Controller) ShowUnreadPage(w http.ResponseWriter, r *http.Request) { builder.WithLimit(nbItemsPerPage) entries, err := builder.GetEntries() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/unread_mark_all_read.go b/ui/unread_mark_all_read.go index c467923..724da67 100644 --- a/ui/unread_mark_all_read.go +++ b/ui/unread_mark_all_read.go @@ -8,7 +8,7 @@ import ( "net/http" "miniflux.app/http/request" - "miniflux.app/http/response" + "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/logger" ) @@ -19,5 +19,5 @@ func (c *Controller) MarkAllAsRead(w http.ResponseWriter, r *http.Request) { logger.Error("[MarkAllAsRead] %v", err) } - response.Redirect(w, r, route.Path(c.router, "unread")) + html.Redirect(w, r, route.Path(c.router, "unread")) } diff --git a/ui/user_create.go b/ui/user_create.go index b31b0de..a94d286 100644 --- a/ui/user_create.go +++ b/ui/user_create.go @@ -21,12 +21,12 @@ func (c *Controller) CreateUser(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if !user.IsAdmin { - html.Forbidden(w) + html.Forbidden(w, r) return } diff --git a/ui/user_edit.go b/ui/user_edit.go index f2c1abf..2348a7d 100644 --- a/ui/user_edit.go +++ b/ui/user_edit.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -21,24 +21,24 @@ func (c *Controller) EditUser(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if !user.IsAdmin { - html.Forbidden(w) + html.Forbidden(w, r) return } userID := request.RouteInt64Param(r, "userID") selectedUser, err := c.store.UserByID(userID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if selectedUser == nil { - html.NotFound(w) + html.NotFound(w, r) return } diff --git a/ui/user_list.go b/ui/user_list.go index dce30e0..3c9e3ed 100644 --- a/ui/user_list.go +++ b/ui/user_list.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" @@ -20,18 +20,18 @@ func (c *Controller) ShowUsers(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if !user.IsAdmin { - html.Forbidden(w) + html.Forbidden(w, r) return } users, err := c.store.Users() if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } diff --git a/ui/user_remove.go b/ui/user_remove.go index 981a7a2..beda7be 100644 --- a/ui/user_remove.go +++ b/ui/user_remove.go @@ -8,7 +8,6 @@ import ( "net/http" "miniflux.app/http/request" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/route" ) @@ -17,31 +16,31 @@ import ( func (c *Controller) RemoveUser(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if !user.IsAdmin { - html.Forbidden(w) + html.Forbidden(w, r) return } userID := request.RouteInt64Param(r, "userID") selectedUser, err := c.store.UserByID(userID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if selectedUser == nil { - html.NotFound(w) + html.NotFound(w, r) return } if err := c.store.RemoveUser(selectedUser.ID); err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } - response.Redirect(w, r, route.Path(c.router, "users")) + html.Redirect(w, r, route.Path(c.router, "users")) } diff --git a/ui/user_save.go b/ui/user_save.go index 7c3685b..a4be346 100644 --- a/ui/user_save.go +++ b/ui/user_save.go @@ -7,7 +7,6 @@ package ui // import "miniflux.app/ui" import ( "net/http" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/request" "miniflux.app/http/route" @@ -21,12 +20,12 @@ import ( func (c *Controller) SaveUser(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if !user.IsAdmin { - html.Forbidden(w) + html.Forbidden(w, r) return } @@ -60,5 +59,5 @@ func (c *Controller) SaveUser(w http.ResponseWriter, r *http.Request) { return } - response.Redirect(w, r, route.Path(c.router, "users")) + html.Redirect(w, r, route.Path(c.router, "users")) } diff --git a/ui/user_update.go b/ui/user_update.go index 006d49a..34567e0 100644 --- a/ui/user_update.go +++ b/ui/user_update.go @@ -2,13 +2,12 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. -package ui // import "miniflux.app/ui" +package ui // import "miniflux.app/ui" import ( "net/http" "miniflux.app/http/request" - "miniflux.app/http/response" "miniflux.app/http/response/html" "miniflux.app/http/route" "miniflux.app/logger" @@ -21,24 +20,24 @@ import ( func (c *Controller) UpdateUser(w http.ResponseWriter, r *http.Request) { user, err := c.store.UserByID(request.UserID(r)) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if !user.IsAdmin { - html.Forbidden(w) + html.Forbidden(w, r) return } userID := request.RouteInt64Param(r, "userID") selectedUser, err := c.store.UserByID(userID) if err != nil { - html.ServerError(w, err) + html.ServerError(w, r, err) return } if selectedUser == nil { - html.NotFound(w) + html.NotFound(w, r) return } @@ -73,5 +72,5 @@ func (c *Controller) UpdateUser(w http.ResponseWriter, r *http.Request) { return } - response.Redirect(w, r, route.Path(c.router, "users")) + html.Redirect(w, r, route.Path(c.router, "users")) } -- cgit v1.2.3