aboutsummaryrefslogtreecommitdiffhomepage
path: root/ui
diff options
context:
space:
mode:
authorGravatar Frédéric Guillot <fred@miniflux.net>2018-04-29 16:35:04 -0700
committerGravatar Frédéric Guillot <fred@miniflux.net>2018-04-29 16:35:04 -0700
commitf49b42f70f902d4da1e0fa4080e99164b331b716 (patch)
treec6bdd19f11d100c44b0d30344ec37038f649e988 /ui
parent1eba1730d1af50ed545f4fde78b22d6fb62ca11e (diff)
Use vanilla HTTP handlers (refactoring)
Diffstat (limited to 'ui')
-rw-r--r--ui/about.go31
-rw-r--r--ui/bookmark_entries.go61
-rw-r--r--ui/category.go257
-rw-r--r--ui/category_create.go33
-rw-r--r--ui/category_edit.go58
-rw-r--r--ui/category_entries.go78
-rw-r--r--ui/category_list.go41
-rw-r--r--ui/category_remove.go50
-rw-r--r--ui/category_save.go71
-rw-r--r--ui/category_update.go79
-rw-r--r--ui/controller.go44
-rw-r--r--ui/entry.go494
-rw-r--r--ui/entry_category.go98
-rw-r--r--ui/entry_feed.go98
-rw-r--r--ui/entry_prev_next.go36
-rw-r--r--ui/entry_read.go81
-rw-r--r--ui/entry_save.go54
-rw-r--r--ui/entry_scraper.go53
-rw-r--r--ui/entry_starred.go91
-rw-r--r--ui/entry_toggle_bookmark.go32
-rw-r--r--ui/entry_unread.go92
-rw-r--r--ui/entry_update_status.go39
-rw-r--r--ui/feed.go236
-rw-r--r--ui/feed_edit.go71
-rw-r--r--ui/feed_entries.go78
-rw-r--r--ui/feed_icon.go36
-rw-r--r--ui/feed_list.go41
-rw-r--r--ui/feed_refresh.go48
-rw-r--r--ui/feed_remove.go32
-rw-r--r--ui/feed_update.go80
-rw-r--r--ui/history.go61
-rw-r--r--ui/history_entries.go59
-rw-r--r--ui/history_flush.go25
-rw-r--r--ui/icon.go33
-rw-r--r--ui/integration_show.go63
-rw-r--r--ui/integration_update.go60
-rw-r--r--ui/integrations.go87
-rw-r--r--ui/login.go80
-rw-r--r--ui/login_check.go66
-rw-r--r--ui/login_show.go29
-rw-r--r--ui/logout.go43
-rw-r--r--ui/oauth2.go155
-rw-r--r--ui/oauth2_callback.go128
-rw-r--r--ui/oauth2_redirect.go38
-rw-r--r--ui/oauth2_unlink.go45
-rw-r--r--ui/opml.go72
-rw-r--r--ui/opml_export.go26
-rw-r--r--ui/opml_import.go33
-rw-r--r--ui/opml_upload.go64
-rw-r--r--ui/payload.go5
-rw-r--r--ui/proxy.go23
-rw-r--r--ui/session.go51
-rw-r--r--ui/session/session.go62
-rw-r--r--ui/session_list.go44
-rw-r--r--ui/session_remove.go34
-rw-r--r--ui/settings.go96
-rw-r--r--ui/settings_show.go54
-rw-r--r--ui/settings_update.go72
-rw-r--r--ui/starred.go68
-rw-r--r--ui/static.go97
-rw-r--r--ui/static_app_icon.go37
-rw-r--r--ui/static_favicon.go28
-rw-r--r--ui/static_javascript.go18
-rw-r--r--ui/static_manifest.go44
-rw-r--r--ui/static_stylesheet.go28
-rw-r--r--ui/subscription.go145
-rw-r--r--ui/subscription_add.go40
-rw-r--r--ui/subscription_bookmarklet.go45
-rw-r--r--ui/subscription_choose.go59
-rw-r--r--ui/subscription_submit.go89
-rw-r--r--ui/unread.go59
-rw-r--r--ui/unread_entries.go63
-rw-r--r--ui/unread_mark_all_read.go23
-rw-r--r--ui/user.go239
-rw-r--r--ui/user_create.go40
-rw-r--r--ui/user_edit.go64
-rw-r--r--ui/user_list.go47
-rw-r--r--ui/user_remove.go55
-rw-r--r--ui/user_save.go65
-rw-r--r--ui/user_update.go84
-rw-r--r--ui/view/view.go39
81 files changed, 3392 insertions, 2285 deletions
diff --git a/ui/about.go b/ui/about.go
index 4de46a8..7618e01 100644
--- a/ui/about.go
+++ b/ui/about.go
@@ -5,21 +5,32 @@
package ui
import (
- "github.com/miniflux/miniflux/http/handler"
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
"github.com/miniflux/miniflux/version"
)
-// AboutPage shows the about page.
-func (c *Controller) AboutPage(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- args, err := c.getCommonTemplateArgs(ctx)
+// About shows the about page.
+func (c *Controller) About(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
if err != nil {
- response.HTML().ServerError(err)
+ html.ServerError(w, err)
return
}
- response.HTML().Render("about", ctx.UserLanguage(), args.Merge(tplParams{
- "version": version.Version,
- "build_date": version.BuildDate,
- "menu": "settings",
- }))
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("version", version.Version)
+ view.Set("build_date", version.BuildDate)
+ view.Set("menu", "settings")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("about"))
}
diff --git a/ui/bookmark_entries.go b/ui/bookmark_entries.go
new file mode 100644
index 0000000..c5f7de3
--- /dev/null
+++ b/ui/bookmark_entries.go
@@ -0,0 +1,61 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowStarredPage renders the page with all starred entries.
+func (c *Controller) ShowStarredPage(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ offset := request.QueryIntParam(r, "offset", 0)
+ builder := c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+ builder.WithStarred()
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(user.EntryDirection)
+ builder.WithOffset(offset)
+ builder.WithLimit(nbItemsPerPage)
+
+ entries, err := builder.GetEntries()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ count, err := builder.CountEntries()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+
+ view.Set("total", count)
+ view.Set("entries", entries)
+ view.Set("pagination", c.getPagination(route.Path(c.router, "starred"), count, offset))
+ view.Set("menu", "starred")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("starred"))
+}
diff --git a/ui/category.go b/ui/category.go
deleted file mode 100644
index 9ffe9a8..0000000
--- a/ui/category.go
+++ /dev/null
@@ -1,257 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package ui
-
-import (
- "errors"
-
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/ui/form"
-)
-
-// ShowCategories shows the page with all categories.
-func (c *Controller) ShowCategories(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- user := ctx.LoggedUser()
- categories, err := c.store.CategoriesWithFeedCount(user.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("categories", ctx.UserLanguage(), args.Merge(tplParams{
- "categories": categories,
- "total": len(categories),
- "menu": "categories",
- }))
-}
-
-// ShowCategoryEntries shows all entries for the given category.
-func (c *Controller) ShowCategoryEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
- offset := request.QueryIntegerParam("offset", 0)
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- category, err := c.getCategoryFromURL(ctx, request, response)
- if err != nil {
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithCategoryID(category.ID)
- builder.WithOrder(model.DefaultSortingOrder)
- builder.WithDirection(user.EntryDirection)
- builder.WithoutStatus(model.EntryStatusRemoved)
- builder.WithOffset(offset)
- builder.WithLimit(nbItemsPerPage)
-
- entries, err := builder.GetEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- count, err := builder.CountEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("category_entries", ctx.UserLanguage(), args.Merge(tplParams{
- "category": category,
- "entries": entries,
- "total": count,
- "pagination": c.getPagination(ctx.Route("categoryEntries", "categoryID", category.ID), count, offset),
- "menu": "categories",
- }))
-}
-
-// CreateCategory shows the form to create a new category.
-func (c *Controller) CreateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("create_category", ctx.UserLanguage(), args.Merge(tplParams{
- "menu": "categories",
- }))
-}
-
-// SaveCategory validate and save the new category into the database.
-func (c *Controller) SaveCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- categoryForm := form.NewCategoryForm(request.Request())
- if err := categoryForm.Validate(); err != nil {
- response.HTML().Render("create_category", ctx.UserLanguage(), args.Merge(tplParams{
- "errorMessage": err.Error(),
- }))
- return
- }
-
- duplicateCategory, err := c.store.CategoryByTitle(user.ID, categoryForm.Title)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if duplicateCategory != nil {
- response.HTML().Render("create_category", ctx.UserLanguage(), args.Merge(tplParams{
- "errorMessage": "This category already exists.",
- }))
- return
- }
-
- category := model.Category{Title: categoryForm.Title, UserID: user.ID}
- err = c.store.CreateCategory(&category)
- if err != nil {
- logger.Info("[Controller:CreateCategory] %v", err)
- response.HTML().Render("create_category", ctx.UserLanguage(), args.Merge(tplParams{
- "errorMessage": "Unable to create this category.",
- }))
- return
- }
-
- response.Redirect(ctx.Route("categories"))
-}
-
-// EditCategory shows the form to modify a category.
-func (c *Controller) EditCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- category, err := c.getCategoryFromURL(ctx, request, response)
- if err != nil {
- logger.Error("[Controller:EditCategory] %v", err)
- return
- }
-
- args, err := c.getCategoryFormTemplateArgs(ctx, user, category, nil)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("edit_category", ctx.UserLanguage(), args)
-}
-
-// UpdateCategory validate and update a category.
-func (c *Controller) UpdateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- category, err := c.getCategoryFromURL(ctx, request, response)
- if err != nil {
- logger.Error("[Controller:UpdateCategory] %v", err)
- return
- }
-
- categoryForm := form.NewCategoryForm(request.Request())
- args, err := c.getCategoryFormTemplateArgs(ctx, user, category, categoryForm)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if err := categoryForm.Validate(); err != nil {
- response.HTML().Render("edit_category", ctx.UserLanguage(), args.Merge(tplParams{
- "errorMessage": err.Error(),
- }))
- return
- }
-
- if c.store.AnotherCategoryExists(user.ID, category.ID, categoryForm.Title) {
- response.HTML().Render("edit_category", ctx.UserLanguage(), args.Merge(tplParams{
- "errorMessage": "This category already exists.",
- }))
- return
- }
-
- err = c.store.UpdateCategory(categoryForm.Merge(category))
- if err != nil {
- logger.Error("[Controller:UpdateCategory] %v", err)
- response.HTML().Render("edit_category", ctx.UserLanguage(), args.Merge(tplParams{
- "errorMessage": "Unable to update this category.",
- }))
- return
- }
-
- response.Redirect(ctx.Route("categories"))
-}
-
-// RemoveCategory delete a category from the database.
-func (c *Controller) RemoveCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- category, err := c.getCategoryFromURL(ctx, request, response)
- if err != nil {
- return
- }
-
- if err := c.store.RemoveCategory(user.ID, category.ID); err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.Redirect(ctx.Route("categories"))
-}
-
-func (c *Controller) getCategoryFromURL(ctx *handler.Context, request *handler.Request, response *handler.Response) (*model.Category, error) {
- categoryID, err := request.IntegerParam("categoryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return nil, err
- }
-
- user := ctx.LoggedUser()
- category, err := c.store.Category(user.ID, categoryID)
- if err != nil {
- response.HTML().ServerError(err)
- return nil, err
- }
-
- if category == nil {
- response.HTML().NotFound()
- return nil, errors.New("Category not found")
- }
-
- return category, nil
-}
-
-func (c *Controller) getCategoryFormTemplateArgs(ctx *handler.Context, user *model.User, category *model.Category, categoryForm *form.CategoryForm) (tplParams, error) {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- return nil, err
- }
-
- if categoryForm == nil {
- args["form"] = form.CategoryForm{
- Title: category.Title,
- }
- } else {
- args["form"] = categoryForm
- }
-
- args["category"] = category
- args["menu"] = "categories"
- return args, nil
-}
diff --git a/ui/category_create.go b/ui/category_create.go
new file mode 100644
index 0000000..6060620
--- /dev/null
+++ b/ui/category_create.go
@@ -0,0 +1,33 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// CreateCategory shows the form to create a new category.
+func (c *Controller) CreateCategory(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("menu", "categories")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("create_category"))
+}
diff --git a/ui/category_edit.go b/ui/category_edit.go
new file mode 100644
index 0000000..df8f619
--- /dev/null
+++ b/ui/category_edit.go
@@ -0,0 +1,58 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// EditCategory shows the form to modify a category.
+func (c *Controller) EditCategory(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ categoryID, err := request.IntParam(r, "categoryID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ category, err := c.store.Category(ctx.UserID(), categoryID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if category == nil {
+ html.NotFound(w)
+ return
+ }
+
+ categoryForm := form.CategoryForm{
+ Title: category.Title,
+ }
+
+ view.Set("form", categoryForm)
+ view.Set("category", category)
+ view.Set("menu", "categories")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("edit_category"))
+}
diff --git a/ui/category_entries.go b/ui/category_entries.go
new file mode 100644
index 0000000..309ddb3
--- /dev/null
+++ b/ui/category_entries.go
@@ -0,0 +1,78 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// CategoryEntries shows all entries for the given category.
+func (c *Controller) CategoryEntries(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ categoryID, err := request.IntParam(r, "categoryID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ category, err := c.store.Category(ctx.UserID(), categoryID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if category == nil {
+ html.NotFound(w)
+ return
+ }
+
+ offset := request.QueryIntParam(r, "offset", 0)
+ builder := c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithCategoryID(category.ID)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(user.EntryDirection)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+ builder.WithOffset(offset)
+ builder.WithLimit(nbItemsPerPage)
+
+ entries, err := builder.GetEntries()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ count, err := builder.CountEntries()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("category", category)
+ view.Set("total", count)
+ view.Set("entries", entries)
+ view.Set("pagination", c.getPagination(route.Path(c.router, "categoryEntries", "categoryID", category.ID), count, offset))
+ view.Set("menu", "categories")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("category_entries"))
+}
diff --git a/ui/category_list.go b/ui/category_list.go
new file mode 100644
index 0000000..c315d77
--- /dev/null
+++ b/ui/category_list.go
@@ -0,0 +1,41 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// CategoryList shows the page with all categories.
+func (c *Controller) CategoryList(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ categories, err := c.store.CategoriesWithFeedCount(user.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("categories", categories)
+ view.Set("total", len(categories))
+ view.Set("menu", "categories")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("categories"))
+}
diff --git a/ui/category_remove.go b/ui/category_remove.go
new file mode 100644
index 0000000..6656ee3
--- /dev/null
+++ b/ui/category_remove.go
@@ -0,0 +1,50 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+)
+
+// RemoveCategory deletes a category from the database.
+func (c *Controller) RemoveCategory(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ categoryID, err := request.IntParam(r, "categoryID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ category, err := c.store.Category(ctx.UserID(), categoryID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if category == nil {
+ html.NotFound(w)
+ return
+ }
+
+ if err := c.store.RemoveCategory(user.ID, category.ID); err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ response.Redirect(w, r, route.Path(c.router, "categories"))
+}
diff --git a/ui/category_save.go b/ui/category_save.go
new file mode 100644
index 0000000..d186b28
--- /dev/null
+++ b/ui/category_save.go
@@ -0,0 +1,71 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// SaveCategory validate and save the new category into the database.
+func (c *Controller) SaveCategory(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ categoryForm := form.NewCategoryForm(r)
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("form", categoryForm)
+ view.Set("menu", "categories")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ if err := categoryForm.Validate(); err != nil {
+ view.Set("errorMessage", err.Error())
+ html.OK(w, view.Render("create_category"))
+ return
+ }
+
+ duplicateCategory, err := c.store.CategoryByTitle(user.ID, categoryForm.Title)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if duplicateCategory != nil {
+ view.Set("errorMessage", "This category already exists.")
+ html.OK(w, view.Render("create_category"))
+ return
+ }
+
+ category := model.Category{
+ Title: categoryForm.Title,
+ UserID: user.ID,
+ }
+
+ if err = c.store.CreateCategory(&category); err != nil {
+ logger.Error("[Controller:CreateCategory] %v", err)
+ view.Set("errorMessage", "Unable to create this category.")
+ html.OK(w, view.Render("create_category"))
+ return
+ }
+
+ response.Redirect(w, r, route.Path(c.router, "categories"))
+}
diff --git a/ui/category_update.go b/ui/category_update.go
new file mode 100644
index 0000000..daa6a02
--- /dev/null
+++ b/ui/category_update.go
@@ -0,0 +1,79 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// UpdateCategory validates and updates a category.
+func (c *Controller) UpdateCategory(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ categoryID, err := request.IntParam(r, "categoryID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ category, err := c.store.Category(ctx.UserID(), categoryID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if category == nil {
+ html.NotFound(w)
+ return
+ }
+
+ categoryForm := form.NewCategoryForm(r)
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("form", categoryForm)
+ view.Set("category", category)
+ view.Set("menu", "categories")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ if err := categoryForm.Validate(); err != nil {
+ view.Set("errorMessage", err.Error())
+ html.OK(w, view.Render("edit_category"))
+ return
+ }
+
+ if c.store.AnotherCategoryExists(user.ID, category.ID, categoryForm.Title) {
+ view.Set("errorMessage", "This category already exists.")
+ html.OK(w, view.Render("edit_category"))
+ return
+ }
+
+ err = c.store.UpdateCategory(categoryForm.Merge(category))
+ if err != nil {
+ logger.Error("[Controller:UpdateCategory] %v", err)
+ view.Set("errorMessage", "Unable to update this category.")
+ html.OK(w, view.Render("edit_category"))
+ return
+ }
+
+ response.Redirect(w, r, route.Path(c.router, "categories"))
+}
diff --git a/ui/controller.go b/ui/controller.go
index a7af48b..b3e85af 100644
--- a/ui/controller.go
+++ b/ui/controller.go
@@ -5,59 +5,35 @@
package ui
import (
+ "github.com/gorilla/mux"
"github.com/miniflux/miniflux/config"
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/locale"
"github.com/miniflux/miniflux/reader/feed"
"github.com/miniflux/miniflux/scheduler"
"github.com/miniflux/miniflux/storage"
+ "github.com/miniflux/miniflux/template"
)
-type tplParams map[string]interface{}
-
-func (t tplParams) Merge(d tplParams) tplParams {
- for k, v := range d {
- t[k] = v
- }
-
- return t
-}
-
// Controller contains all HTTP handlers for the user interface.
type Controller struct {
cfg *config.Config
store *storage.Storage
pool *scheduler.WorkerPool
feedHandler *feed.Handler
-}
-
-func (c *Controller) getCommonTemplateArgs(ctx *handler.Context) (tplParams, error) {
- user := ctx.LoggedUser()
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithStatus(model.EntryStatusUnread)
-
- countUnread, err := builder.CountEntries()
- if err != nil {
- return nil, err
- }
-
- params := tplParams{
- "menu": "",
- "user": user,
- "countUnread": countUnread,
- "csrf": ctx.CSRF(),
- "flashMessage": ctx.FlashMessage(),
- "flashErrorMessage": ctx.FlashErrorMessage(),
- }
- return params, nil
+ tpl *template.Engine
+ router *mux.Router
+ translator *locale.Translator
}
// NewController returns a new Controller.
-func NewController(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler) *Controller {
+func NewController(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler, tpl *template.Engine, translator *locale.Translator, router *mux.Router) *Controller {
return &Controller{
cfg: cfg,
store: store,
pool: pool,
feedHandler: feedHandler,
+ tpl: tpl,
+ translator: translator,
+ router: router,
}
}
diff --git a/ui/entry.go b/ui/entry.go
deleted file mode 100644
index 436f189..0000000
--- a/ui/entry.go
+++ /dev/null
@@ -1,494 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package ui
-
-import (
- "errors"
-
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/integration"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/reader/sanitizer"
- "github.com/miniflux/miniflux/reader/scraper"
- "github.com/miniflux/miniflux/storage"
-)
-
-// FetchContent downloads the original HTML page and returns relevant contents.
-func (c *Controller) FetchContent(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- user := ctx.LoggedUser()
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- if entry == nil {
- response.JSON().NotFound(errors.New("Entry not found"))
- return
- }
-
- content, err := scraper.Fetch(entry.URL, entry.Feed.ScraperRules)
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- entry.Content = sanitizer.Sanitize(entry.URL, content)
- c.store.UpdateEntryContent(entry)
-
- response.JSON().Created(map[string]string{"content": entry.Content})
-}
-
-// SaveEntry send the link to external services.
-func (c *Controller) SaveEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- user := ctx.LoggedUser()
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- if entry == nil {
- response.JSON().NotFound(errors.New("Entry not found"))
- return
- }
-
- settings, err := c.store.Integration(user.ID)
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- go func() {
- integration.SendEntry(entry, settings)
- }()
-
- response.JSON().Created(map[string]string{"message": "saved"})
-}
-
-// ShowFeedEntry shows a single feed entry in "feed" mode.
-func (c *Controller) ShowFeedEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- feedID, err := request.IntegerParam("feedID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithFeedID(feedID)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if entry == nil {
- response.HTML().NotFound()
- return
- }
-
- if entry.Status == model.EntryStatusUnread {
- err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
- if err != nil {
- logger.Error("[Controller:ShowFeedEntry] %v", err)
- response.HTML().ServerError(nil)
- return
- }
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- builder = c.store.NewEntryQueryBuilder(user.ID)
- builder.WithFeedID(feedID)
-
- prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- nextEntryRoute := ""
- if nextEntry != nil {
- nextEntryRoute = ctx.Route("feedEntry", "feedID", feedID, "entryID", nextEntry.ID)
- }
-
- prevEntryRoute := ""
- if prevEntry != nil {
- prevEntryRoute = ctx.Route("feedEntry", "feedID", feedID, "entryID", prevEntry.ID)
- }
-
- response.HTML().Render("entry", ctx.UserLanguage(), args.Merge(tplParams{
- "entry": entry,
- "prevEntry": prevEntry,
- "nextEntry": nextEntry,
- "nextEntryRoute": nextEntryRoute,
- "prevEntryRoute": prevEntryRoute,
- "menu": "feeds",
- }))
-}
-
-// ShowCategoryEntry shows a single feed entry in "category" mode.
-func (c *Controller) ShowCategoryEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- categoryID, err := request.IntegerParam("categoryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithCategoryID(categoryID)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if entry == nil {
- response.HTML().NotFound()
- return
- }
-
- if entry.Status == model.EntryStatusUnread {
- err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
- if err != nil {
- logger.Error("[Controller:ShowCategoryEntry] %v", err)
- response.HTML().ServerError(nil)
- return
- }
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- builder = c.store.NewEntryQueryBuilder(user.ID)
- builder.WithCategoryID(categoryID)
-
- prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- nextEntryRoute := ""
- if nextEntry != nil {
- nextEntryRoute = ctx.Route("categoryEntry", "categoryID", categoryID, "entryID", nextEntry.ID)
- }
-
- prevEntryRoute := ""
- if prevEntry != nil {
- prevEntryRoute = ctx.Route("categoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID)
- }
-
- response.HTML().Render("entry", ctx.UserLanguage(), args.Merge(tplParams{
- "entry": entry,
- "prevEntry": prevEntry,
- "nextEntry": nextEntry,
- "nextEntryRoute": nextEntryRoute,
- "prevEntryRoute": prevEntryRoute,
- "menu": "categories",
- }))
-}
-
-// ShowUnreadEntry shows a single feed entry in "unread" mode.
-func (c *Controller) ShowUnreadEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if entry == nil {
- response.HTML().NotFound()
- return
- }
-
- builder = c.store.NewEntryQueryBuilder(user.ID)
- builder.WithStatus(model.EntryStatusUnread)
-
- prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- nextEntryRoute := ""
- if nextEntry != nil {
- nextEntryRoute = ctx.Route("unreadEntry", "entryID", nextEntry.ID)
- }
-
- prevEntryRoute := ""
- if prevEntry != nil {
- prevEntryRoute = ctx.Route("unreadEntry", "entryID", prevEntry.ID)
- }
-
- // We change the status here, otherwise we cannot get the pagination for unread items.
- if entry.Status == model.EntryStatusUnread {
- err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
- if err != nil {
- logger.Error("[Controller:ShowUnreadEntry] %v", err)
- response.HTML().ServerError(nil)
- return
- }
- }
-
- // The unread counter have to be fetched after changing the entry status
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("entry", ctx.UserLanguage(), args.Merge(tplParams{
- "entry": entry,
- "prevEntry": prevEntry,
- "nextEntry": nextEntry,
- "nextEntryRoute": nextEntryRoute,
- "prevEntryRoute": prevEntryRoute,
- "menu": "unread",
- }))
-}
-
-// ShowReadEntry shows a single feed entry in "history" mode.
-func (c *Controller) ShowReadEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if entry == nil {
- response.HTML().NotFound()
- return
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- builder = c.store.NewEntryQueryBuilder(user.ID)
- builder.WithStatus(model.EntryStatusRead)
-
- prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- nextEntryRoute := ""
- if nextEntry != nil {
- nextEntryRoute = ctx.Route("readEntry", "entryID", nextEntry.ID)
- }
-
- prevEntryRoute := ""
- if prevEntry != nil {
- prevEntryRoute = ctx.Route("readEntry", "entryID", prevEntry.ID)
- }
-
- response.HTML().Render("entry", ctx.UserLanguage(), args.Merge(tplParams{
- "entry": entry,
- "prevEntry": prevEntry,
- "nextEntry": nextEntry,
- "nextEntryRoute": nextEntryRoute,
- "prevEntryRoute": prevEntryRoute,
- "menu": "history",
- }))
-}
-
-// ShowStarredEntry shows a single feed entry in "starred" mode.
-func (c *Controller) ShowStarredEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if entry == nil {
- response.HTML().NotFound()
- return
- }
-
- if entry.Status == model.EntryStatusUnread {
- err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
- if err != nil {
- logger.Error("[Controller:ShowReadEntry] %v", err)
- response.HTML().ServerError(nil)
- return
- }
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- builder = c.store.NewEntryQueryBuilder(user.ID)
- builder.WithStarred()
-
- prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- nextEntryRoute := ""
- if nextEntry != nil {
- nextEntryRoute = ctx.Route("starredEntry", "entryID", nextEntry.ID)
- }
-
- prevEntryRoute := ""
- if prevEntry != nil {
- prevEntryRoute = ctx.Route("starredEntry", "entryID", prevEntry.ID)
- }
-
- response.HTML().Render("entry", ctx.UserLanguage(), args.Merge(tplParams{
- "entry": entry,
- "prevEntry": prevEntry,
- "nextEntry": nextEntry,
- "nextEntryRoute": nextEntryRoute,
- "prevEntryRoute": prevEntryRoute,
- "menu": "starred",
- }))
-}
-
-// UpdateEntriesStatus handles Ajax request to update the status for a list of entries.
-func (c *Controller) UpdateEntriesStatus(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- entryIDs, status, err := decodeEntryStatusPayload(request.Body())
- if err != nil {
- logger.Error("[Controller:UpdateEntryStatus] %v", err)
- response.JSON().BadRequest(nil)
- return
- }
-
- if len(entryIDs) == 0 {
- response.JSON().BadRequest(errors.New("The list of entryID is empty"))
- return
- }
-
- err = c.store.SetEntriesStatus(user.ID, entryIDs, status)
- if err != nil {
- logger.Error("[Controller:UpdateEntryStatus] %v", err)
- response.JSON().ServerError(nil)
- return
- }
-
- response.JSON().Standard("OK")
-}
-
-func (c *Controller) getEntryPrevNext(user *model.User, builder *storage.EntryQueryBuilder, entryID int64) (prev *model.Entry, next *model.Entry, err error) {
- builder.WithoutStatus(model.EntryStatusRemoved)
- builder.WithOrder(model.DefaultSortingOrder)
- builder.WithDirection(user.EntryDirection)
-
- entries, err := builder.GetEntries()
- if err != nil {
- return nil, nil, err
- }
-
- n := len(entries)
- for i := 0; i < n; i++ {
- if entries[i].ID == entryID {
- if i-1 >= 0 {
- prev = entries[i-1]
- }
-
- if i+1 < n {
- next = entries[i+1]
- }
- }
- }
-
- return prev, next, nil
-}
diff --git a/ui/entry_category.go b/ui/entry_category.go
new file mode 100644
index 0000000..a8766bb
--- /dev/null
+++ b/ui/entry_category.go
@@ -0,0 +1,98 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowCategoryEntry shows a single feed entry in "category" mode.
+func (c *Controller) ShowCategoryEntry(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ categoryID, err := request.IntParam(r, "categoryID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ entryID, err := request.IntParam(r, "entryID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ builder := c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithCategoryID(categoryID)
+ builder.WithEntryID(entryID)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+
+ entry, err := builder.GetEntry()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if entry == nil {
+ html.NotFound(w)
+ return
+ }
+
+ if entry.Status == model.EntryStatusUnread {
+ err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+ if err != nil {
+ logger.Error("[Controller:ShowCategoryEntry] %v", err)
+ html.ServerError(w, nil)
+ return
+ }
+ }
+
+ builder = c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithCategoryID(categoryID)
+
+ prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ nextEntryRoute := ""
+ if nextEntry != nil {
+ nextEntryRoute = route.Path(c.router, "categoryEntry", "categoryID", categoryID, "entryID", nextEntry.ID)
+ }
+
+ prevEntryRoute := ""
+ if prevEntry != nil {
+ prevEntryRoute = route.Path(c.router, "categoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID)
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("entry", entry)
+ view.Set("prevEntry", prevEntry)
+ view.Set("nextEntry", nextEntry)
+ view.Set("nextEntryRoute", nextEntryRoute)
+ view.Set("prevEntryRoute", prevEntryRoute)
+ view.Set("menu", "categories")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("entry"))
+}
diff --git a/ui/entry_feed.go b/ui/entry_feed.go
new file mode 100644
index 0000000..dfa85bc
--- /dev/null
+++ b/ui/entry_feed.go
@@ -0,0 +1,98 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowFeedEntry shows a single feed entry in "feed" mode.
+func (c *Controller) ShowFeedEntry(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ entryID, err := request.IntParam(r, "entryID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ feedID, err := request.IntParam(r, "feedID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ builder := c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithFeedID(feedID)
+ builder.WithEntryID(entryID)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+
+ entry, err := builder.GetEntry()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if entry == nil {
+ html.NotFound(w)
+ return
+ }
+
+ if entry.Status == model.EntryStatusUnread {
+ err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+ if err != nil {
+ logger.Error("[Controller:ShowFeedEntry] %v", err)
+ html.ServerError(w, nil)
+ return
+ }
+ }
+
+ builder = c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithFeedID(feedID)
+
+ prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ nextEntryRoute := ""
+ if nextEntry != nil {
+ nextEntryRoute = route.Path(c.router, "feedEntry", "feedID", feedID, "entryID", nextEntry.ID)
+ }
+
+ prevEntryRoute := ""
+ if prevEntry != nil {
+ prevEntryRoute = route.Path(c.router, "feedEntry", "feedID", feedID, "entryID", prevEntry.ID)
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("entry", entry)
+ view.Set("prevEntry", prevEntry)
+ view.Set("nextEntry", nextEntry)
+ view.Set("nextEntryRoute", nextEntryRoute)
+ view.Set("prevEntryRoute", prevEntryRoute)
+ view.Set("menu", "feeds")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("entry"))
+}
diff --git a/ui/entry_prev_next.go b/ui/entry_prev_next.go
new file mode 100644
index 0000000..9ab415e
--- /dev/null
+++ b/ui/entry_prev_next.go
@@ -0,0 +1,36 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/storage"
+)
+
+func (c *Controller) getEntryPrevNext(user *model.User, builder *storage.EntryQueryBuilder, entryID int64) (prev *model.Entry, next *model.Entry, err error) {
+ builder.WithoutStatus(model.EntryStatusRemoved)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(user.EntryDirection)
+
+ entries, err := builder.GetEntries()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ n := len(entries)
+ for i := 0; i < n; i++ {
+ if entries[i].ID == entryID {
+ if i-1 >= 0 {
+ prev = entries[i-1]
+ }
+
+ if i+1 < n {
+ next = entries[i+1]
+ }
+ }
+ }
+
+ return prev, next, nil
+}
diff --git a/ui/entry_read.go b/ui/entry_read.go
new file mode 100644
index 0000000..40c5074
--- /dev/null
+++ b/ui/entry_read.go
@@ -0,0 +1,81 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowReadEntry shows a single feed entry in "history" mode.
+func (c *Controller) ShowReadEntry(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ entryID, err := request.IntParam(r, "entryID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ builder := c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithEntryID(entryID)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+
+ entry, err := builder.GetEntry()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if entry == nil {
+ html.NotFound(w)
+ return
+ }
+
+ builder = c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithStatus(model.EntryStatusRead)
+
+ prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ nextEntryRoute := ""
+ if nextEntry != nil {
+ nextEntryRoute = route.Path(c.router, "readEntry", "entryID", nextEntry.ID)
+ }
+
+ prevEntryRoute := ""
+ if prevEntry != nil {
+ prevEntryRoute = route.Path(c.router, "readEntry", "entryID", prevEntry.ID)
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("entry", entry)
+ view.Set("prevEntry", prevEntry)
+ view.Set("nextEntry", nextEntry)
+ view.Set("nextEntryRoute", nextEntryRoute)
+ view.Set("prevEntryRoute", prevEntryRoute)
+ view.Set("menu", "history")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("entry"))
+}
diff --git a/ui/entry_save.go b/ui/entry_save.go
new file mode 100644
index 0000000..9b24078
--- /dev/null
+++ b/ui/entry_save.go
@@ -0,0 +1,54 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/json"
+ "github.com/miniflux/miniflux/integration"
+ "github.com/miniflux/miniflux/model"
+)
+
+// SaveEntry send the link to external services.
+func (c *Controller) SaveEntry(w http.ResponseWriter, r *http.Request) {
+ entryID, err := request.IntParam(r, "entryID")
+ if err != nil {
+ json.BadRequest(w, err)
+ return
+ }
+
+ ctx := context.New(r)
+
+ builder := c.store.NewEntryQueryBuilder(ctx.UserID())
+ builder.WithEntryID(entryID)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+
+ entry, err := builder.GetEntry()
+ if err != nil {
+ json.ServerError(w, err)
+ return
+ }
+
+ if entry == nil {
+ json.NotFound(w, errors.New("Entry not found"))
+ return
+ }
+
+ settings, err := c.store.Integration(ctx.UserID())
+ if err != nil {
+ json.ServerError(w, err)
+ return
+ }
+
+ go func() {
+ integration.SendEntry(entry, settings)
+ }()
+
+ json.Created(w, map[string]string{"message": "saved"})
+}
diff --git a/ui/entry_scraper.go b/ui/entry_scraper.go
new file mode 100644
index 0000000..75e4040
--- /dev/null
+++ b/ui/entry_scraper.go
@@ -0,0 +1,53 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/json"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/reader/sanitizer"
+ "github.com/miniflux/miniflux/reader/scraper"
+)
+
+// FetchContent downloads the original HTML page and returns relevant contents.
+func (c *Controller) FetchContent(w http.ResponseWriter, r *http.Request) {
+ entryID, err := request.IntParam(r, "entryID")
+ if err != nil {
+ json.BadRequest(w, err)
+ return
+ }
+
+ ctx := context.New(r)
+ builder := c.store.NewEntryQueryBuilder(ctx.UserID())
+ builder.WithEntryID(entryID)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+
+ entry, err := builder.GetEntry()
+ if err != nil {
+ json.ServerError(w, err)
+ return
+ }
+
+ if entry == nil {
+ json.NotFound(w, errors.New("Entry not found"))
+ return
+ }
+
+ content, err := scraper.Fetch(entry.URL, entry.Feed.ScraperRules)
+ if err != nil {
+ json.ServerError(w, err)
+ return
+ }
+
+ entry.Content = sanitizer.Sanitize(entry.URL, content)
+ c.store.UpdateEntryContent(entry)
+
+ json.Created(w, map[string]string{"content": entry.Content})
+}
diff --git a/ui/entry_starred.go b/ui/entry_starred.go
new file mode 100644
index 0000000..66ca85d
--- /dev/null
+++ b/ui/entry_starred.go
@@ -0,0 +1,91 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowStarredEntry shows a single feed entry in "starred" mode.
+func (c *Controller) ShowStarredEntry(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ entryID, err := request.IntParam(r, "entryID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ builder := c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithEntryID(entryID)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+
+ entry, err := builder.GetEntry()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if entry == nil {
+ html.NotFound(w)
+ return
+ }
+
+ if entry.Status == model.EntryStatusUnread {
+ err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+ if err != nil {
+ logger.Error("[Controller:ShowReadEntry] %v", err)
+ html.ServerError(w, nil)
+ return
+ }
+ }
+
+ builder = c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithStarred()
+
+ prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ nextEntryRoute := ""
+ if nextEntry != nil {
+ nextEntryRoute = route.Path(c.router, "starredEntry", "entryID", nextEntry.ID)
+ }
+
+ prevEntryRoute := ""
+ if prevEntry != nil {
+ prevEntryRoute = route.Path(c.router, "starredEntry", "entryID", prevEntry.ID)
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("entry", entry)
+ view.Set("prevEntry", prevEntry)
+ view.Set("nextEntry", nextEntry)
+ view.Set("nextEntryRoute", nextEntryRoute)
+ view.Set("prevEntryRoute", prevEntryRoute)
+ view.Set("menu", "starred")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("entry"))
+}
diff --git a/ui/entry_toggle_bookmark.go b/ui/entry_toggle_bookmark.go
new file mode 100644
index 0000000..423fde8
--- /dev/null
+++ b/ui/entry_toggle_bookmark.go
@@ -0,0 +1,32 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/json"
+ "github.com/miniflux/miniflux/logger"
+)
+
+// ToggleBookmark handles Ajax request to toggle bookmark value.
+func (c *Controller) ToggleBookmark(w http.ResponseWriter, r *http.Request) {
+ entryID, err := request.IntParam(r, "entryID")
+ if err != nil {
+ json.BadRequest(w, err)
+ return
+ }
+
+ ctx := context.New(r)
+ if err := c.store.ToggleBookmark(ctx.UserID(), entryID); err != nil {
+ logger.Error("[Controller:ToggleBookmark] %v", err)
+ json.ServerError(w, nil)
+ return
+ }
+
+ json.OK(w, "OK")
+}
diff --git a/ui/entry_unread.go b/ui/entry_unread.go
new file mode 100644
index 0000000..d103e6d
--- /dev/null
+++ b/ui/entry_unread.go
@@ -0,0 +1,92 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowUnreadEntry shows a single feed entry in "unread" mode.
+func (c *Controller) ShowUnreadEntry(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ entryID, err := request.IntParam(r, "entryID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ builder := c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithEntryID(entryID)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+
+ entry, err := builder.GetEntry()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if entry == nil {
+ html.NotFound(w)
+ return
+ }
+
+ builder = c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithStatus(model.EntryStatusUnread)
+
+ prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ nextEntryRoute := ""
+ if nextEntry != nil {
+ nextEntryRoute = route.Path(c.router, "unreadEntry", "entryID", nextEntry.ID)
+ }
+
+ prevEntryRoute := ""
+ if prevEntry != nil {
+ prevEntryRoute = route.Path(c.router, "unreadEntry", "entryID", prevEntry.ID)
+ }
+
+ // We change the status here, otherwise we cannot get the pagination for unread items.
+ if entry.Status == model.EntryStatusUnread {
+ err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+ if err != nil {
+ logger.Error("[Controller:ShowUnreadEntry] %v", err)
+ html.ServerError(w, nil)
+ return
+ }
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("entry", entry)
+ view.Set("prevEntry", prevEntry)
+ view.Set("nextEntry", nextEntry)
+ view.Set("nextEntryRoute", nextEntryRoute)
+ view.Set("prevEntryRoute", prevEntryRoute)
+ view.Set("menu", "unread")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("entry"))
+}
diff --git a/ui/entry_update_status.go b/ui/entry_update_status.go
new file mode 100644
index 0000000..3174d85
--- /dev/null
+++ b/ui/entry_update_status.go
@@ -0,0 +1,39 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response/json"
+ "github.com/miniflux/miniflux/logger"
+)
+
+// UpdateEntriesStatus updates the status for a list of entries.
+func (c *Controller) UpdateEntriesStatus(w http.ResponseWriter, r *http.Request) {
+ entryIDs, status, err := decodeEntryStatusPayload(r.Body)
+ if err != nil {
+ logger.Error("[Controller:UpdateEntryStatus] %v", err)
+ json.BadRequest(w, nil)
+ return
+ }
+
+ if len(entryIDs) == 0 {
+ json.BadRequest(w, errors.New("The list of entryID is empty"))
+ return
+ }
+
+ ctx := context.New(r)
+ err = c.store.SetEntriesStatus(ctx.UserID(), entryIDs, status)
+ if err != nil {
+ logger.Error("[Controller:UpdateEntryStatus] %v", err)
+ json.ServerError(w, nil)
+ return
+ }
+
+ json.OK(w, "OK")
+}
diff --git a/ui/feed.go b/ui/feed.go
deleted file mode 100644
index ae0a54e..0000000
--- a/ui/feed.go
+++ /dev/null
@@ -1,236 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package ui
-
-import (
- "errors"
-
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/ui/form"
-)
-
-// RefreshAllFeeds refresh all feeds in the background for the current user.
-func (c *Controller) RefreshAllFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
- jobs, err := c.store.NewUserBatch(user.ID, c.store.CountFeeds(user.ID))
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- go func() {
- c.pool.Push(jobs)
- }()
-
- response.Redirect(ctx.Route("feeds"))
-}
-
-// ShowFeedsPage shows the page with all subscriptions.
-func (c *Controller) ShowFeedsPage(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- feeds, err := c.store.Feeds(user.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("feeds", ctx.UserLanguage(), args.Merge(tplParams{
- "feeds": feeds,
- "total": len(feeds),
- "menu": "feeds",
- }))
-}
-
-// ShowFeedEntries shows all entries for the given feed.
-func (c *Controller) ShowFeedEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
- offset := request.QueryIntegerParam("offset", 0)
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- feed, err := c.getFeedFromURL(request, response, user)
- if err != nil {
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithFeedID(feed.ID)
- builder.WithoutStatus(model.EntryStatusRemoved)
- builder.WithOrder(model.DefaultSortingOrder)
- builder.WithDirection(user.EntryDirection)
- builder.WithOffset(offset)
- builder.WithLimit(nbItemsPerPage)
-
- entries, err := builder.GetEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- count, err := builder.CountEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("feed_entries", ctx.UserLanguage(), args.Merge(tplParams{
- "feed": feed,
- "entries": entries,
- "total": count,
- "pagination": c.getPagination(ctx.Route("feedEntries", "feedID", feed.ID), count, offset),
- "menu": "feeds",
- }))
-}
-
-// EditFeed shows the form to modify a subscription.
-func (c *Controller) EditFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- feed, err := c.getFeedFromURL(request, response, user)
- if err != nil {
- return
- }
-
- args, err := c.getFeedFormTemplateArgs(ctx, user, feed, nil)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("edit_feed", ctx.UserLanguage(), args)
-}
-
-// UpdateFeed update a subscription and redirect to the feed entries page.
-func (c *Controller) UpdateFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- feed, err := c.getFeedFromURL(request, response, user)
- if err != nil {
- return
- }
-
- feedForm := form.NewFeedForm(request.Request())
- args, err := c.getFeedFormTemplateArgs(ctx, user, feed, feedForm)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if err := feedForm.ValidateModification(); err != nil {
- response.HTML().Render("edit_feed", ctx.UserLanguage(), args.Merge(tplParams{
- "errorMessage": err.Error(),
- }))
- return
- }
-
- err = c.store.UpdateFeed(feedForm.Merge(feed))
- if err != nil {
- logger.Error("[Controller:EditFeed] %v", err)
- response.HTML().Render("edit_feed", ctx.UserLanguage(), args.Merge(tplParams{
- "errorMessage": "Unable to update this feed.",
- }))
- return
- }
-
- response.Redirect(ctx.Route("feedEntries", "feedID", feed.ID))
-}
-
-// RemoveFeed delete a subscription from the database and redirect to the list of feeds page.
-func (c *Controller) RemoveFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- feedID, err := request.IntegerParam("feedID")
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- user := ctx.LoggedUser()
- if err := c.store.RemoveFeed(user.ID, feedID); err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.Redirect(ctx.Route("feeds"))
-}
-
-// RefreshFeed refresh a subscription and redirect to the feed entries page.
-func (c *Controller) RefreshFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- feedID, err := request.IntegerParam("feedID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- user := ctx.LoggedUser()
- if err := c.feedHandler.RefreshFeed(user.ID, feedID); err != nil {
- logger.Error("[Controller:RefreshFeed] %v", err)
- }
-
- response.Redirect(ctx.Route("feedEntries", "feedID", feedID))
-}
-
-func (c *Controller) getFeedFromURL(request *handler.Request, response *handler.Response, user *model.User) (*model.Feed, error) {
- feedID, err := request.IntegerParam("feedID")
- if err != nil {
- response.HTML().BadRequest(err)
- return nil, err
- }
-
- feed, err := c.store.FeedByID(user.ID, feedID)
- if err != nil {
- response.HTML().ServerError(err)
- return nil, err
- }
-
- if feed == nil {
- response.HTML().NotFound()
- return nil, errors.New("Feed not found")
- }
-
- return feed, nil
-}
-
-func (c *Controller) getFeedFormTemplateArgs(ctx *handler.Context, user *model.User, feed *model.Feed, feedForm *form.FeedForm) (tplParams, error) {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- return nil, err
- }
-
- categories, err := c.store.Categories(user.ID)
- if err != nil {
- return nil, err
- }
-
- if feedForm == nil {
- args["form"] = form.FeedForm{
- SiteURL: feed.SiteURL,
- FeedURL: feed.FeedURL,
- Title: feed.Title,
- ScraperRules: feed.ScraperRules,
- RewriteRules: feed.RewriteRules,
- Crawler: feed.Crawler,
- CategoryID: feed.Category.ID,
- }
- } else {
- args["form"] = feedForm
- }
-
- args["categories"] = categories
- args["feed"] = feed
- args["menu"] = "feeds"
- return args, nil
-}
diff --git a/ui/feed_edit.go b/ui/feed_edit.go
new file mode 100644
index 0000000..d6062c8
--- /dev/null
+++ b/ui/feed_edit.go
@@ -0,0 +1,71 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// EditFeed shows the form to modify a subscription.
+func (c *Controller) EditFeed(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ feedID, err := request.IntParam(r, "feedID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ feed, err := c.store.FeedByID(user.ID, feedID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if feed == nil {
+ html.NotFound(w)
+ return
+ }
+
+ categories, err := c.store.Categories(user.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ feedForm := form.FeedForm{
+ SiteURL: feed.SiteURL,
+ FeedURL: feed.FeedURL,
+ Title: feed.Title,
+ ScraperRules: feed.ScraperRules,
+ RewriteRules: feed.RewriteRules,
+ Crawler: feed.Crawler,
+ CategoryID: feed.Category.ID,
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("form", feedForm)
+ view.Set("categories", categories)
+ view.Set("feed", feed)
+ view.Set("menu", "feeds")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("edit_feed"))
+}
diff --git a/ui/feed_entries.go b/ui/feed_entries.go
new file mode 100644
index 0000000..0143562
--- /dev/null
+++ b/ui/feed_entries.go
@@ -0,0 +1,78 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowFeedEntries shows all entries for the given feed.
+func (c *Controller) ShowFeedEntries(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ feedID, err := request.IntParam(r, "feedID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ feed, err := c.store.FeedByID(user.ID, feedID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if feed == nil {
+ html.NotFound(w)
+ return
+ }
+
+ offset := request.QueryIntParam(r, "offset", 0)
+ builder := c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithFeedID(feed.ID)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(user.EntryDirection)
+ builder.WithOffset(offset)
+ builder.WithLimit(nbItemsPerPage)
+
+ entries, err := builder.GetEntries()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ count, err := builder.CountEntries()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("feed", feed)
+ view.Set("entries", entries)
+ view.Set("total", count)
+ view.Set("pagination", c.getPagination(route.Path(c.router, "feedEntries", "feedID", feed.ID), count, offset))
+ view.Set("menu", "feeds")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("feed_entries"))
+}
diff --git a/ui/feed_icon.go b/ui/feed_icon.go
new file mode 100644
index 0000000..c05efe5
--- /dev/null
+++ b/ui/feed_icon.go
@@ -0,0 +1,36 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+)
+
+// ShowIcon shows the feed icon.
+func (c *Controller) ShowIcon(w http.ResponseWriter, r *http.Request) {
+ iconID, err := request.IntParam(r, "iconID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ icon, err := c.store.IconByID(iconID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if icon == nil {
+ html.NotFound(w)
+ return
+ }
+
+ response.Cache(w, r, icon.MimeType, icon.Hash, icon.Content, 72*time.Hour)
+}
diff --git a/ui/feed_list.go b/ui/feed_list.go
new file mode 100644
index 0000000..fcf315c
--- /dev/null
+++ b/ui/feed_list.go
@@ -0,0 +1,41 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowFeedsPage shows the page with all subscriptions.
+func (c *Controller) ShowFeedsPage(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ feeds, err := c.store.Feeds(user.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("feeds", feeds)
+ view.Set("total", len(feeds))
+ view.Set("menu", "feeds")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("feeds"))
+}
diff --git a/ui/feed_refresh.go b/ui/feed_refresh.go
new file mode 100644
index 0000000..eb86b04
--- /dev/null
+++ b/ui/feed_refresh.go
@@ -0,0 +1,48 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+)
+
+// RefreshFeed refresh a subscription and redirect to the feed entries page.
+func (c *Controller) RefreshFeed(w http.ResponseWriter, r *http.Request) {
+ feedID, err := request.IntParam(r, "feedID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ ctx := context.New(r)
+ if err := c.feedHandler.RefreshFeed(ctx.UserID(), feedID); err != nil {
+ logger.Error("[Controller:RefreshFeed] %v", err)
+ }
+
+ response.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feedID))
+}
+
+// RefreshAllFeeds refresh all feeds in the background for the current user.
+func (c *Controller) RefreshAllFeeds(w http.ResponseWriter, r *http.Request) {
+ userID := context.New(r).UserID()
+ jobs, err := c.store.NewUserBatch(userID, c.store.CountFeeds(userID))
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ go func() {
+ c.pool.Push(jobs)
+ }()
+
+ response.Redirect(w, r, route.Path(c.router, "feeds"))
+}
diff --git a/ui/feed_remove.go b/ui/feed_remove.go
new file mode 100644
index 0000000..23d877b
--- /dev/null
+++ b/ui/feed_remove.go
@@ -0,0 +1,32 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+)
+
+// RemoveFeed deletes a subscription from the database and redirect to the list of feeds page.
+func (c *Controller) RemoveFeed(w http.ResponseWriter, r *http.Request) {
+ feedID, err := request.IntParam(r, "feedID")
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ ctx := context.New(r)
+ if err := c.store.RemoveFeed(ctx.UserID(), feedID); err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ response.Redirect(w, r, route.Path(c.router, "feeds"))
+}
diff --git a/ui/feed_update.go b/ui/feed_update.go
new file mode 100644
index 0000000..8157327
--- /dev/null
+++ b/ui/feed_update.go
@@ -0,0 +1,80 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// UpdateFeed update a subscription and redirect to the feed entries page.
+func (c *Controller) UpdateFeed(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ feedID, err := request.IntParam(r, "feedID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ feed, err := c.store.FeedByID(user.ID, feedID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if feed == nil {
+ html.NotFound(w)
+ return
+ }
+
+ categories, err := c.store.Categories(user.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ feedForm := form.NewFeedForm(r)
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("form", feedForm)
+ view.Set("categories", categories)
+ view.Set("feed", feed)
+ view.Set("menu", "feeds")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ if err := feedForm.ValidateModification(); err != nil {
+ view.Set("errorMessage", err.Error())
+ html.OK(w, view.Render("edit_feed"))
+ return
+ }
+
+ err = c.store.UpdateFeed(feedForm.Merge(feed))
+ if err != nil {
+ logger.Error("[Controller:EditFeed] %v", err)
+ view.Set("errorMessage", "Unable to update this feed.")
+ html.OK(w, view.Render("edit_feed"))
+ return
+ }
+
+ response.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID))
+}
diff --git a/ui/history.go b/ui/history.go
deleted file mode 100644
index f9c8ab5..0000000
--- a/ui/history.go
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package ui
-
-import (
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/model"
-)
-
-// ShowHistoryPage renders the page with all read entries.
-func (c *Controller) ShowHistoryPage(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
- offset := request.QueryIntegerParam("offset", 0)
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithStatus(model.EntryStatusRead)
- builder.WithOrder(model.DefaultSortingOrder)
- builder.WithDirection(user.EntryDirection)
- builder.WithOffset(offset)
- builder.WithLimit(nbItemsPerPage)
-
- entries, err := builder.GetEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- count, err := builder.CountEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("history", ctx.UserLanguage(), args.Merge(tplParams{
- "entries": entries,
- "total": count,
- "pagination": c.getPagination(ctx.Route("history"), count, offset),
- "menu": "history",
- }))
-}
-
-// FlushHistory changes all "read" items to "removed".
-func (c *Controller) FlushHistory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- err := c.store.FlushHistory(user.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.Redirect(ctx.Route("history"))
-}
diff --git a/ui/history_entries.go b/ui/history_entries.go
new file mode 100644
index 0000000..e695ce3
--- /dev/null
+++ b/ui/history_entries.go
@@ -0,0 +1,59 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowHistoryPage renders the page with all read entries.
+func (c *Controller) ShowHistoryPage(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ offset := request.QueryIntParam(r, "offset", 0)
+ builder := c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithStatus(model.EntryStatusRead)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(user.EntryDirection)
+ builder.WithOffset(offset)
+ builder.WithLimit(nbItemsPerPage)
+
+ entries, err := builder.GetEntries()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ count, err := builder.CountEntries()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("entries", entries)
+ view.Set("total", count)
+ view.Set("pagination", c.getPagination(route.Path(c.router, "history"), count, offset))
+ view.Set("menu", "history")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("history"))
+}
diff --git a/ui/history_flush.go b/ui/history_flush.go
new file mode 100644
index 0000000..96b6f3d
--- /dev/null
+++ b/ui/history_flush.go
@@ -0,0 +1,25 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+)
+
+// FlushHistory changes all "read" items to "removed".
+func (c *Controller) FlushHistory(w http.ResponseWriter, r *http.Request) {
+ err := c.store.FlushHistory(context.New(r).UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ response.Redirect(w, r, route.Path(c.router, "history"))
+}
diff --git a/ui/icon.go b/ui/icon.go
deleted file mode 100644
index 4c445f0..0000000
--- a/ui/icon.go
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package ui
-
-import (
- "time"
-
- "github.com/miniflux/miniflux/http/handler"
-)
-
-// ShowIcon shows the feed icon.
-func (c *Controller) ShowIcon(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- iconID, err := request.IntegerParam("iconID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- icon, err := c.store.IconByID(iconID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if icon == nil {
- response.HTML().NotFound()
- return
- }
-
- response.Cache(icon.MimeType, icon.Hash, icon.Content, 72*time.Hour)
-}
diff --git a/ui/integration_show.go b/ui/integration_show.go
new file mode 100644
index 0000000..703f610
--- /dev/null
+++ b/ui/integration_show.go
@@ -0,0 +1,63 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowIntegrations renders the page with all external integrations.
+func (c *Controller) ShowIntegrations(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ integration, err := c.store.Integration(user.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ integrationForm := form.IntegrationForm{
+ PinboardEnabled: integration.PinboardEnabled,
+ PinboardToken: integration.PinboardToken,
+ PinboardTags: integration.PinboardTags,
+ PinboardMarkAsUnread: integration.PinboardMarkAsUnread,
+ InstapaperEnabled: integration.InstapaperEnabled,
+ InstapaperUsername: integration.InstapaperUsername,
+ InstapaperPassword: integration.InstapaperPassword,
+ FeverEnabled: integration.FeverEnabled,
+ FeverUsername: integration.FeverUsername,
+ FeverPassword: integration.FeverPassword,
+ WallabagEnabled: integration.WallabagEnabled,
+ WallabagURL: integration.WallabagURL,
+ WallabagClientID: integration.WallabagClientID,
+ WallabagClientSecret: integration.WallabagClientSecret,
+ WallabagUsername: integration.WallabagUsername,
+ WallabagPassword: integration.WallabagPassword,
+ NunuxKeeperEnabled: integration.NunuxKeeperEnabled,
+ NunuxKeeperURL: integration.NunuxKeeperURL,
+ NunuxKeeperAPIKey: integration.NunuxKeeperAPIKey,
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("form", integrationForm)
+ view.Set("menu", "settings")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("integrations"))
+}
diff --git a/ui/integration_update.go b/ui/integration_update.go
new file mode 100644
index 0000000..a1e98cb
--- /dev/null
+++ b/ui/integration_update.go
@@ -0,0 +1,60 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "crypto/md5"
+ "fmt"
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+)
+
+// UpdateIntegration updates integration settings.
+func (c *Controller) UpdateIntegration(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ integration, err := c.store.Integration(user.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ integrationForm := form.NewIntegrationForm(r)
+ integrationForm.Merge(integration)
+
+ if integration.FeverUsername != "" && c.store.HasDuplicateFeverUsername(user.ID, integration.FeverUsername) {
+ sess.NewFlashErrorMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("There is already someone else with the same Fever username!"))
+ response.Redirect(w, r, route.Path(c.router, "integrations"))
+ return
+ }
+
+ if integration.FeverEnabled {
+ integration.FeverToken = fmt.Sprintf("%x", md5.Sum([]byte(integration.FeverUsername+":"+integration.FeverPassword)))
+ } else {
+ integration.FeverToken = ""
+ }
+
+ err = c.store.UpdateIntegration(integration)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ sess.NewFlashMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("Preferences saved!"))
+ response.Redirect(w, r, route.Path(c.router, "integrations"))
+}
diff --git a/ui/integrations.go b/ui/integrations.go
deleted file mode 100644
index a980f9b..0000000
--- a/ui/integrations.go
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package ui
-
-import (
- "crypto/md5"
- "fmt"
-
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/ui/form"
-)
-
-// ShowIntegrations renders the page with all external integrations.
-func (c *Controller) ShowIntegrations(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
- integration, err := c.store.Integration(user.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("integrations", ctx.UserLanguage(), args.Merge(tplParams{
- "menu": "settings",
- "form": form.IntegrationForm{
- PinboardEnabled: integration.PinboardEnabled,
- PinboardToken: integration.PinboardToken,
- PinboardTags: integration.PinboardTags,
- PinboardMarkAsUnread: integration.PinboardMarkAsUnread,
- InstapaperEnabled: integration.InstapaperEnabled,
- InstapaperUsername: integration.InstapaperUsername,
- InstapaperPassword: integration.InstapaperPassword,
- FeverEnabled: integration.FeverEnabled,
- FeverUsername: integration.FeverUsername,
- FeverPassword: integration.FeverPassword,
- WallabagEnabled: integration.WallabagEnabled,
- WallabagURL: integration.WallabagURL,
- WallabagClientID: integration.WallabagClientID,
- WallabagClientSecret: integration.WallabagClientSecret,
- WallabagUsername: integration.WallabagUsername,
- WallabagPassword: integration.WallabagPassword,
- NunuxKeeperEnabled: integration.NunuxKeeperEnabled,
- NunuxKeeperURL: integration.NunuxKeeperURL,
- NunuxKeeperAPIKey: integration.NunuxKeeperAPIKey,
- },
- }))
-}
-
-// UpdateIntegration updates integration settings.
-func (c *Controller) UpdateIntegration(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
- integration, err := c.store.Integration(user.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- integrationForm := form.NewIntegrationForm(request.Request())
- integrationForm.Merge(integration)
-
- if integration.FeverUsername != "" && c.store.HasDuplicateFeverUsername(user.ID, integration.FeverUsername) {
- ctx.SetFlashErrorMessage(ctx.Translate("There is already someone else with the same Fever username!"))
- response.Redirect(ctx.Route("integrations"))
- return
- }
-
- if integration.FeverEnabled {
- integration.FeverToken = fmt.Sprintf("%x", md5.Sum([]byte(integration.FeverUsername+":"+integration.FeverPassword)))
- } else {
- integration.FeverToken = ""
- }
-
- err = c.store.UpdateIntegration(integration)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.Redirect(ctx.Route("integrations"))
-}
diff --git a/ui/login.go b/ui/login.go
deleted file mode 100644
index 18571d8..0000000
--- a/ui/login.go
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package ui
-
-import (
- "github.com/miniflux/miniflux/http/cookie"
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/ui/form"
-
- "github.com/tomasen/realip"
-)
-
-// ShowLoginPage shows the login form.
-func (c *Controller) ShowLoginPage(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- if ctx.IsAuthenticated() {
- response.Redirect(ctx.Route("unread"))
- return
- }
-
- response.HTML().Render("login", ctx.UserLanguage(), tplParams{
- "csrf": ctx.CSRF(),
- })
-}
-
-// CheckLogin validates the username/password and redirects the user to the unread page.
-func (c *Controller) CheckLogin(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- authForm := form.NewAuthForm(request.Request())
- tplParams := tplParams{
- "errorMessage": "Invalid username or password.",
- "csrf": ctx.CSRF(),
- "form": authForm,
- }
-
- if err := authForm.Validate(); err != nil {
- logger.Error("[Controller:CheckLogin] %v", err)
- response.HTML().Render("login", ctx.UserLanguage(), tplParams)
- return
- }
-
- if err := c.store.CheckPassword(authForm.Username, authForm.Password); err != nil {
- logger.Error("[Controller:CheckLogin] %v", err)
- response.HTML().Render("login", ctx.UserLanguage(), tplParams)
- return
- }
-
- sessionToken, err := c.store.CreateUserSession(
- authForm.Username,
- request.Request().UserAgent(),
- realip.RealIP(request.Request()),
- )
-
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- logger.Info("[Controller:CheckLogin] username=%s just logged in", authForm.Username)
-
- response.SetCookie(cookie.New(cookie.CookieUserSessionID, sessionToken, c.cfg.IsHTTPS, c.cfg.BasePath()))
- response.Redirect(ctx.Route("unread"))
-}
-
-// Logout destroy the session and redirects the user to the login page.
-func (c *Controller) Logout(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- if err := c.store.UpdateSessionField(ctx.SessionID(), "language", user.Language); err != nil {
- logger.Error("[Controller:Logout] %v", err)
- }
-
- if err := c.store.RemoveUserSessionByToken(user.ID, ctx.UserSessionToken()); err != nil {
- logger.Error("[Controller:Logout] %v", err)
- }
-
- response.SetCookie(cookie.Expired(cookie.CookieUserSessionID, c.cfg.IsHTTPS, c.cfg.BasePath()))
- response.Redirect(ctx.Route("login"))
-}
diff --git a/ui/login_check.go b/ui/login_check.go
new file mode 100644
index 0000000..bc49c8d
--- /dev/null
+++ b/ui/login_check.go
@@ -0,0 +1,66 @@
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/cookie"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+ "github.com/tomasen/realip"
+)
+
+// CheckLogin validates the username/password and redirects the user to the unread page.
+func (c *Controller) CheckLogin(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+
+ authForm := form.NewAuthForm(r)
+
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("errorMessage", "Invalid username or password.")
+ view.Set("form", authForm)
+
+ if err := authForm.Validate(); err != nil {
+ logger.Error("[Controller:CheckLogin] %v", err)
+ html.OK(w, view.Render("login"))
+ return
+ }
+
+ if err := c.store.CheckPassword(authForm.Username, authForm.Password); err != nil {
+ logger.Error("[Controller:CheckLogin] %v", err)
+ html.OK(w, view.Render("login"))
+ return
+ }
+
+ sessionToken, userID, err := c.store.CreateUserSession(authForm.Username, r.UserAgent(), realip.RealIP(r))
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ logger.Info("[Controller:CheckLogin] username=%s just logged in", authForm.Username)
+ c.store.SetLastLogin(userID)
+
+ userLanguage, err := c.store.UserLanguage(userID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ sess.SetLanguage(userLanguage)
+
+ http.SetCookie(w, cookie.New(
+ cookie.CookieUserSessionID,
+ sessionToken,
+ c.cfg.IsHTTPS,
+ c.cfg.BasePath(),
+ ))
+
+ response.Redirect(w, r, route.Path(c.router, "unread"))
+}
diff --git a/ui/login_show.go b/ui/login_show.go
new file mode 100644
index 0000000..84dc160
--- /dev/null
+++ b/ui/login_show.go
@@ -0,0 +1,29 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowLoginPage shows the login form.
+func (c *Controller) ShowLoginPage(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ if ctx.IsAuthenticated() {
+ response.Redirect(w, r, route.Path(c.router, "unread"))
+ return
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ html.OK(w, view.Render("login"))
+}
diff --git a/ui/logout.go b/ui/logout.go
new file mode 100644
index 0000000..2946d1a
--- /dev/null
+++ b/ui/logout.go
@@ -0,0 +1,43 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/cookie"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/ui/session"
+)
+
+// Logout destroy the session and redirects the user to the login page.
+func (c *Controller) Logout(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ sess.SetLanguage(user.Language)
+
+ if err := c.store.RemoveUserSessionByToken(user.ID, ctx.UserSessionToken()); err != nil {
+ logger.Error("[Controller:Logout] %v", err)
+ }
+
+ http.SetCookie(w, cookie.Expired(
+ cookie.CookieUserSessionID,
+ c.cfg.IsHTTPS,
+ c.cfg.BasePath(),
+ ))
+
+ response.Redirect(w, r, route.Path(c.router, "login"))
+}
diff --git a/ui/oauth2.go b/ui/oauth2.go
index c5bf931..f1e6a4e 100644
--- a/ui/oauth2.go
+++ b/ui/oauth2.go
@@ -1,4 +1,4 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
@@ -6,162 +6,9 @@ package ui
import (
"github.com/miniflux/miniflux/config"
- "github.com/miniflux/miniflux/http/cookie"
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
"github.com/miniflux/miniflux/oauth2"
-
- "github.com/tomasen/realip"
)
-// OAuth2Redirect redirects the user to the consent page to ask for permission.
-func (c *Controller) OAuth2Redirect(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- provider := request.StringParam("provider", "")
- if provider == "" {
- logger.Error("[OAuth2] Invalid or missing provider: %s", provider)
- response.Redirect(ctx.Route("login"))
- return
- }
-
- authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
- if err != nil {
- logger.Error("[OAuth2] %v", err)
- response.Redirect(ctx.Route("login"))
- return
- }
-
- response.Redirect(authProvider.GetRedirectURL(ctx.GenerateOAuth2State()))
-}
-
-// OAuth2Callback receives the authorization code and create a new session.
-func (c *Controller) OAuth2Callback(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- provider := request.StringParam("provider", "")
- if provider == "" {
- logger.Error("[OAuth2] Invalid or missing provider")
- response.Redirect(ctx.Route("login"))
- return
- }
-
- code := request.QueryStringParam("code", "")
- if code == "" {
- logger.Error("[OAuth2] No code received on callback")
- response.Redirect(ctx.Route("login"))
- return
- }
-
- state := request.QueryStringParam("state", "")
- if state == "" || state != ctx.OAuth2State() {
- logger.Error(`[OAuth2] Invalid state value: got "%s" instead of "%s"`, state, ctx.OAuth2State())
- response.Redirect(ctx.Route("login"))
- return
- }
-
- authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
- if err != nil {
- logger.Error("[OAuth2] %v", err)
- response.Redirect(ctx.Route("login"))
- return
- }
-
- profile, err := authProvider.GetProfile(code)
- if err != nil {
- logger.Error("[OAuth2] %v", err)
- response.Redirect(ctx.Route("login"))
- return
- }
-
- if ctx.IsAuthenticated() {
- user, err := c.store.UserByExtraField(profile.Key, profile.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if user != nil {
- logger.Error("[OAuth2] User #%d cannot be associated because %s is already associated", ctx.UserID(), user.Username)
- ctx.SetFlashErrorMessage(ctx.Translate("There is already someone associated with this provider!"))
- response.Redirect(ctx.Route("settings"))
- return
- }
-
- user = ctx.LoggedUser()
- if err := c.store.UpdateExtraField(user.ID, profile.Key, profile.ID); err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- ctx.SetFlashMessage(ctx.Translate("Your external account is now linked !"))
- response.Redirect(ctx.Route("settings"))
- return
- }
-
- user, err := c.store.UserByExtraField(profile.Key, profile.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if user == nil {
- if !c.cfg.IsOAuth2UserCreationAllowed() {
- response.HTML().Forbidden()
- return
- }
-
- user = model.NewUser()
- user.Username = profile.Username
- user.IsAdmin = false
- user.Extra[profile.Key] = profile.ID
-
- if err := c.store.CreateUser(user); err != nil {
- response.HTML().ServerError(err)
- return
- }
- }
-
- sessionToken, err := c.store.CreateUserSession(
- user.Username,
- request.Request().UserAgent(),
- realip.RealIP(request.Request()),
- )
-
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- logger.Info("[Controller:OAuth2Callback] username=%s just logged in", user.Username)
-
- response.SetCookie(cookie.New(cookie.CookieUserSessionID, sessionToken, c.cfg.IsHTTPS, c.cfg.BasePath()))
- response.Redirect(ctx.Route("unread"))
-}
-
-// OAuth2Unlink unlink an account from the external provider.
-func (c *Controller) OAuth2Unlink(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- provider := request.StringParam("provider", "")
- if provider == "" {
- logger.Info("[OAuth2] Invalid or missing provider")
- response.Redirect(ctx.Route("login"))
- return
- }
-
- authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
- if err != nil {
- logger.Error("[OAuth2] %v", err)
- response.Redirect(ctx.Route("settings"))
- return
- }
-
- user := ctx.LoggedUser()
- if err := c.store.RemoveExtraField(user.ID, authProvider.GetUserExtraKey()); err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.Redirect(ctx.Route("settings"))
- return
-}
-
func getOAuth2Manager(cfg *config.Config) *oauth2.Manager {
return oauth2.NewManager(
cfg.OAuth2ClientID(),
diff --git a/ui/oauth2_callback.go b/ui/oauth2_callback.go
new file mode 100644
index 0000000..f564c7e
--- /dev/null
+++ b/ui/oauth2_callback.go
@@ -0,0 +1,128 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/cookie"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/ui/session"
+
+ "github.com/tomasen/realip"
+)
+
+// OAuth2Callback receives the authorization code and create a new session.
+func (c *Controller) OAuth2Callback(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+
+ provider := request.Param(r, "provider", "")
+ if provider == "" {
+ logger.Error("[OAuth2] Invalid or missing provider")
+ response.Redirect(w, r, route.Path(c.router, "login"))
+ return
+ }
+
+ code := request.QueryParam(r, "code", "")
+ if code == "" {
+ logger.Error("[OAuth2] No code received on callback")
+ response.Redirect(w, r, route.Path(c.router, "login"))
+ return
+ }
+
+ state := request.QueryParam(r, "state", "")
+ if state == "" || state != ctx.OAuth2State() {
+ logger.Error(`[OAuth2] Invalid state value: got "%s" instead of "%s"`, state, ctx.OAuth2State())
+ response.Redirect(w, r, route.Path(c.router, "login"))
+ return
+ }
+
+ authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
+ if err != nil {
+ logger.Error("[OAuth2] %v", err)
+ response.Redirect(w, r, route.Path(c.router, "login"))
+ return
+ }
+
+ profile, err := authProvider.GetProfile(code)
+ if err != nil {
+ logger.Error("[OAuth2] %v", err)
+ response.Redirect(w, r, route.Path(c.router, "login"))
+ return
+ }
+
+ if ctx.IsAuthenticated() {
+ user, err := c.store.UserByExtraField(profile.Key, profile.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if user != nil {
+ logger.Error("[OAuth2] User #%d cannot be associated because %s is already associated", ctx.UserID(), user.Username)
+ sess.NewFlashErrorMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("There is already someone associated with this provider!"))
+ response.Redirect(w, r, route.Path(c.router, "settings"))
+ return
+ }
+
+ if err := c.store.UpdateExtraField(ctx.UserID(), profile.Key, profile.ID); err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ sess.NewFlashMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("Your external account is now linked!"))
+ response.Redirect(w, r, route.Path(c.router, "settings"))
+ return
+ }
+
+ user, err := c.store.UserByExtraField(profile.Key, profile.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if user == nil {
+ if !c.cfg.IsOAuth2UserCreationAllowed() {
+ html.Forbidden(w)
+ return
+ }
+
+ user = model.NewUser()
+ user.Username = profile.Username
+ user.IsAdmin = false
+ user.Extra[profile.Key] = profile.ID
+
+ if err := c.store.CreateUser(user); err != nil {
+ html.ServerError(w, err)
+ return
+ }
+ }
+
+ sessionToken, _, err := c.store.CreateUserSession(user.Username, r.UserAgent(), realip.RealIP(r))
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ logger.Info("[Controller:OAuth2Callback] username=%s just logged in", user.Username)
+ c.store.SetLastLogin(user.ID)
+ sess.SetLanguage(user.Language)
+
+ http.SetCookie(w, cookie.New(
+ cookie.CookieUserSessionID,
+ sessionToken,
+ c.cfg.IsHTTPS,
+ c.cfg.BasePath(),
+ ))
+
+ response.Redirect(w, r, route.Path(c.router, "unread"))
+}
diff --git a/ui/oauth2_redirect.go b/ui/oauth2_redirect.go
new file mode 100644
index 0000000..7f472ac
--- /dev/null
+++ b/ui/oauth2_redirect.go
@@ -0,0 +1,38 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/ui/session"
+)
+
+// OAuth2Redirect redirects the user to the consent page to ask for permission.
+func (c *Controller) OAuth2Redirect(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+
+ provider := request.Param(r, "provider", "")
+ if provider == "" {
+ logger.Error("[OAuth2] Invalid or missing provider: %s", provider)
+ response.Redirect(w, r, route.Path(c.router, "login"))
+ return
+ }
+
+ authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
+ if err != nil {
+ logger.Error("[OAuth2] %v", err)
+ response.Redirect(w, r, route.Path(c.router, "login"))
+ return
+ }
+
+ response.Redirect(w, r, authProvider.GetRedirectURL(sess.NewOAuth2State()))
+}
diff --git a/ui/oauth2_unlink.go b/ui/oauth2_unlink.go
new file mode 100644
index 0000000..e67c6fe
--- /dev/null
+++ b/ui/oauth2_unlink.go
@@ -0,0 +1,45 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/ui/session"
+)
+
+// OAuth2Unlink unlink an account from the external provider.
+func (c *Controller) OAuth2Unlink(w http.ResponseWriter, r *http.Request) {
+ provider := request.Param(r, "provider", "")
+ if provider == "" {
+ logger.Info("[OAuth2] Invalid or missing provider")
+ response.Redirect(w, r, route.Path(c.router, "login"))
+ return
+ }
+
+ authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
+ if err != nil {
+ logger.Error("[OAuth2] %v", err)
+ response.Redirect(w, r, route.Path(c.router, "settings"))
+ return
+ }
+
+ ctx := context.New(r)
+ if err := c.store.RemoveExtraField(ctx.UserID(), authProvider.GetUserExtraKey()); err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ sess := session.New(c.store, ctx)
+ sess.NewFlashMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("Your external account is now dissociated!"))
+ response.Redirect(w, r, route.Path(c.router, "settings"))
+ return
+}
diff --git a/ui/opml.go b/ui/opml.go
deleted file mode 100644
index 3ca68ff..0000000
--- a/ui/opml.go
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package ui
-
-import (
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/reader/opml"
-)
-
-// Export generates the OPML file.
-func (c *Controller) Export(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
- opml, err := opml.NewHandler(c.store).Export(user.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.XML().Download("feeds.opml", opml)
-}
-
-// Import shows the import form.
-func (c *Controller) Import(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("import", ctx.UserLanguage(), args.Merge(tplParams{
- "menu": "feeds",
- }))
-}
-
-// UploadOPML handles OPML file importation.
-func (c *Controller) UploadOPML(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- file, fileHeader, err := request.File("file")
- if err != nil {
- logger.Error("[Controller:UploadOPML] %v", err)
- response.Redirect(ctx.Route("import"))
- return
- }
- defer file.Close()
-
- user := ctx.LoggedUser()
- logger.Info(
- "[Controller:UploadOPML] User #%d uploaded this file: %s (%d bytes)",
- user.ID,
- fileHeader.Filename,
- fileHeader.Size,
- )
-
- if impErr := opml.NewHandler(c.store).Import(user.ID, file); impErr != nil {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("import", ctx.UserLanguage(), args.Merge(tplParams{
- "errorMessage": impErr,
- "menu": "feeds",
- }))
-
- return
- }
-
- response.Redirect(ctx.Route("feeds"))
-}
diff --git a/ui/opml_export.go b/ui/opml_export.go
new file mode 100644
index 0000000..ec396f0
--- /dev/null
+++ b/ui/opml_export.go
@@ -0,0 +1,26 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/response/xml"
+ "github.com/miniflux/miniflux/reader/opml"
+)
+
+// Export generates the OPML file.
+func (c *Controller) Export(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ opml, err := opml.NewHandler(c.store).Export(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ xml.Attachment(w, "feeds.opml", opml)
+}
diff --git a/ui/opml_import.go b/ui/opml_import.go
new file mode 100644
index 0000000..fd3a869
--- /dev/null
+++ b/ui/opml_import.go
@@ -0,0 +1,33 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// Import shows the import form.
+func (c *Controller) Import(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("menu", "feeds")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("import"))
+}
diff --git a/ui/opml_upload.go b/ui/opml_upload.go
new file mode 100644
index 0000000..ef93fb1
--- /dev/null
+++ b/ui/opml_upload.go
@@ -0,0 +1,64 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/reader/opml"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// UploadOPML handles OPML file importation.
+func (c *Controller) UploadOPML(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ file, fileHeader, err := r.FormFile("file")
+ if err != nil {
+ logger.Error("[Controller:UploadOPML] %v", err)
+ response.Redirect(w, r, route.Path(c.router, "import"))
+ return
+ }
+ defer file.Close()
+
+ logger.Info(
+ "[Controller:UploadOPML] User #%d uploaded this file: %s (%d bytes)",
+ user.ID,
+ fileHeader.Filename,
+ fileHeader.Size,
+ )
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("menu", "feeds")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ if fileHeader.Size == 0 {
+ view.Set("errorMessage", "This file is empty")
+ html.OK(w, view.Render("import"))
+ return
+ }
+
+ if impErr := opml.NewHandler(c.store).Import(user.ID, file); impErr != nil {
+ view.Set("errorMessage", impErr)
+ html.OK(w, view.Render("import"))
+ return
+ }
+
+ response.Redirect(w, r, route.Path(c.router, "feeds"))
+}
diff --git a/ui/payload.go b/ui/payload.go
index 2841828..bd65c83 100644
--- a/ui/payload.go
+++ b/ui/payload.go
@@ -12,14 +12,15 @@ import (
"github.com/miniflux/miniflux/model"
)
-func decodeEntryStatusPayload(data io.Reader) (entryIDs []int64, status string, err error) {
+func decodeEntryStatusPayload(r io.ReadCloser) (entryIDs []int64, status string, err error) {
type payload struct {
EntryIDs []int64 `json:"entry_ids"`
Status string `json:"status"`
}
var p payload
- decoder := json.NewDecoder(data)
+ decoder := json.NewDecoder(r)
+ defer r.Close()
if err = decoder.Decode(&p); err != nil {
return nil, "", fmt.Errorf("invalid JSON payload: %v", err)
}
diff --git a/ui/proxy.go b/ui/proxy.go
index 78ab67d..62364aa 100644
--- a/ui/proxy.go
+++ b/ui/proxy.go
@@ -8,31 +8,34 @@ import (
"encoding/base64"
"errors"
"io/ioutil"
+ "net/http"
"time"
"github.com/miniflux/miniflux/crypto"
"github.com/miniflux/miniflux/http/client"
- "github.com/miniflux/miniflux/http/handler"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
"github.com/miniflux/miniflux/logger"
)
// ImageProxy fetch an image from a remote server and sent it back to the browser.
-func (c *Controller) ImageProxy(ctx *handler.Context, request *handler.Request, response *handler.Response) {
+func (c *Controller) ImageProxy(w http.ResponseWriter, r *http.Request) {
// If we receive a "If-None-Match" header we assume the image in stored in browser cache
- if request.Request().Header.Get("If-None-Match") != "" {
- response.NotModified()
+ if r.Header.Get("If-None-Match") != "" {
+ response.NotModified(w)
return
}
- encodedURL := request.StringParam("encodedURL", "")
+ encodedURL := request.Param(r, "encodedURL", "")
if encodedURL == "" {
- response.HTML().BadRequest(errors.New("No URL provided"))
+ html.BadRequest(w, errors.New("No URL provided"))
return
}
decodedURL, err := base64.URLEncoding.DecodeString(encodedURL)
if err != nil {
- response.HTML().BadRequest(errors.New("Unable to decode this URL"))
+ html.BadRequest(w, errors.New("Unable to decode this URL"))
return
}
@@ -40,17 +43,17 @@ func (c *Controller) ImageProxy(ctx *handler.Context, request *handler.Request,
resp, err := clt.Get()
if err != nil {
logger.Error("[Controller:ImageProxy] %v", err)
- response.HTML().NotFound()
+ html.NotFound(w)
return
}
if resp.HasServerFailure() {
- response.HTML().NotFound()
+ html.NotFound(w)
return
}
body, _ := ioutil.ReadAll(resp.Body)
etag := crypto.HashFromBytes(body)
- response.Cache(resp.ContentType, etag, body, 72*time.Hour)
+ response.Cache(w, r, resp.ContentType, etag, body, 72*time.Hour)
}
diff --git a/ui/session.go b/ui/session.go
deleted file mode 100644
index 49f81d1..0000000
--- a/ui/session.go
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package ui
-
-import (
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/logger"
-)
-
-// ShowSessions shows the list of active user sessions.
-func (c *Controller) ShowSessions(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- sessions, err := c.store.UserSessions(user.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- sessions.UseTimezone(user.Timezone)
- response.HTML().Render("sessions", ctx.UserLanguage(), args.Merge(tplParams{
- "sessions": sessions,
- "currentSessionToken": ctx.UserSessionToken(),
- "menu": "settings",
- }))
-}
-
-// RemoveSession remove a user session.
-func (c *Controller) RemoveSession(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- sessionID, err := request.IntegerParam("sessionID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- err = c.store.RemoveUserSessionByID(user.ID, sessionID)
- if err != nil {
- logger.Error("[Controller:RemoveSession] %v", err)
- }
-
- response.Redirect(ctx.Route("sessions"))
-}
diff --git a/ui/session/session.go b/ui/session/session.go
new file mode 100644
index 0000000..fc25406
--- /dev/null
+++ b/ui/session/session.go
@@ -0,0 +1,62 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package session
+
+import (
+ "github.com/miniflux/miniflux/crypto"
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/storage"
+)
+
+// Session handles session data.
+type Session struct {
+ store *storage.Storage
+ ctx *context.Context
+}
+
+// NewOAuth2State generates a new OAuth2 state and stores the value into the database.
+func (s *Session) NewOAuth2State() string {
+ state := crypto.GenerateRandomString(32)
+ s.store.UpdateSessionField(s.ctx.SessionID(), "oauth2_state", state)
+ return state
+}
+
+// NewFlashMessage creates a new flash message.
+func (s *Session) NewFlashMessage(message string) {
+ s.store.UpdateSessionField(s.ctx.SessionID(), "flash_message", message)
+}
+
+// FlashMessage returns the current flash message if any.
+func (s *Session) FlashMessage() string {
+ message := s.ctx.FlashMessage()
+ if message != "" {
+ s.store.UpdateSessionField(s.ctx.SessionID(), "flash_message", "")
+ }
+ return message
+}
+
+// NewFlashErrorMessage creates a new flash error message.
+func (s *Session) NewFlashErrorMessage(message string) {
+ s.store.UpdateSessionField(s.ctx.SessionID(), "flash_error_message", message)
+}
+
+// FlashErrorMessage returns the last flash error message if any.
+func (s *Session) FlashErrorMessage() string {
+ message := s.ctx.FlashErrorMessage()
+ if message != "" {
+ s.store.UpdateSessionField(s.ctx.SessionID(), "flash_error_message", "")
+ }
+ return message
+}
+
+// SetLanguage updates language field in session.
+func (s *Session) SetLanguage(language string) {
+ s.store.UpdateSessionField(s.ctx.SessionID(), "language", language)
+}
+
+// New returns a new session handler.
+func New(store *storage.Storage, ctx *context.Context) *Session {
+ return &Session{store, ctx}
+}
diff --git a/ui/session_list.go b/ui/session_list.go
new file mode 100644
index 0000000..cd9bfe8
--- /dev/null
+++ b/ui/session_list.go
@@ -0,0 +1,44 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response/html"
+)
+
+// ShowSessions shows the list of active user sessions.
+func (c *Controller) ShowSessions(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ sessions, err := c.store.UserSessions(user.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ sessions.UseTimezone(user.Timezone)
+
+ view.Set("currentSessionToken", ctx.UserSessionToken())
+ view.Set("sessions", sessions)
+ view.Set("menu", "settings")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("sessions"))
+}
diff --git a/ui/session_remove.go b/ui/session_remove.go
new file mode 100644
index 0000000..1ee2a6b
--- /dev/null
+++ b/ui/session_remove.go
@@ -0,0 +1,34 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+)
+
+// RemoveSession remove a user session.
+func (c *Controller) RemoveSession(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ sessionID, err := request.IntParam(r, "sessionID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ err = c.store.RemoveUserSessionByID(ctx.UserID(), sessionID)
+ if err != nil {
+ logger.Error("[Controller:RemoveSession] %v", err)
+ }
+
+ response.Redirect(w, r, route.Path(c.router, "sessions"))
+}
diff --git a/ui/settings.go b/ui/settings.go
deleted file mode 100644
index 88dc155..0000000
--- a/ui/settings.go
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package ui
-
-import (
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/locale"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/ui/form"
-)
-
-// ShowSettings shows the settings page.
-func (c *Controller) ShowSettings(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- args, err := c.getSettingsFormTemplateArgs(ctx, user, nil)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("settings", ctx.UserLanguage(), args)
-}
-
-// UpdateSettings update the settings.
-func (c *Controller) UpdateSettings(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- settingsForm := form.NewSettingsForm(request.Request())
- args, err := c.getSettingsFormTemplateArgs(ctx, user, settingsForm)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if err := settingsForm.Validate(); err != nil {
- response.HTML().Render("settings", ctx.UserLanguage(), args.Merge(tplParams{
- "form": settingsForm,
- "errorMessage": err.Error(),
- }))
- return
- }
-
- if c.store.AnotherUserExists(user.ID, settingsForm.Username) {
- response.HTML().Render("settings", ctx.UserLanguage(), args.Merge(tplParams{
- "form": settingsForm,
- "errorMessage": "This user already exists.",
- }))
- return
- }
-
- err = c.store.UpdateUser(settingsForm.Merge(user))
- if err != nil {
- logger.Error("[Controller:UpdateSettings] %v", err)
- response.HTML().Render("settings", ctx.UserLanguage(), args.Merge(tplParams{
- "form": settingsForm,
- "errorMessage": "Unable to update this user.",
- }))
- return
- }
-
- ctx.SetFlashMessage(ctx.Translate("Preferences saved!"))
- response.Redirect(ctx.Route("settings"))
-}
-
-func (c *Controller) getSettingsFormTemplateArgs(ctx *handler.Context, user *model.User, settingsForm *form.SettingsForm) (tplParams, error) {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- return args, err
- }
-
- if settingsForm == nil {
- args["form"] = form.SettingsForm{
- Username: user.Username,
- Theme: user.Theme,
- Language: user.Language,
- Timezone: user.Timezone,
- EntryDirection: user.EntryDirection,
- }
- } else {
- args["form"] = settingsForm
- }
-
- args["menu"] = "settings"
- args["themes"] = model.Themes()
- args["languages"] = locale.AvailableLanguages()
- args["timezones"], err = c.store.Timezones()
- if err != nil {
- return args, err
- }
-
- return args, nil
-}
diff --git a/ui/settings_show.go b/ui/settings_show.go
new file mode 100644
index 0000000..c23473b
--- /dev/null
+++ b/ui/settings_show.go
@@ -0,0 +1,54 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/locale"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowSettings shows the settings page.
+func (c *Controller) ShowSettings(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ settingsForm := form.SettingsForm{
+ Username: user.Username,
+ Theme: user.Theme,
+ Language: user.Language,
+ Timezone: user.Timezone,
+ EntryDirection: user.EntryDirection,
+ }
+
+ timezones, err := c.store.Timezones()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ view.Set("form", settingsForm)
+ view.Set("themes", model.Themes())
+ view.Set("languages", locale.AvailableLanguages())
+ view.Set("timezones", timezones)
+ view.Set("menu", "settings")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("settings"))
+}
diff --git a/ui/settings_update.go b/ui/settings_update.go
new file mode 100644
index 0000000..a8b49ff
--- /dev/null
+++ b/ui/settings_update.go
@@ -0,0 +1,72 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/locale"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// UpdateSettings update the settings.
+func (c *Controller) UpdateSettings(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ timezones, err := c.store.Timezones()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ settingsForm := form.NewSettingsForm(r)
+
+ view.Set("form", settingsForm)
+ view.Set("themes", model.Themes())
+ view.Set("languages", locale.AvailableLanguages())
+ view.Set("timezones", timezones)
+ view.Set("menu", "settings")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ if err := settingsForm.Validate(); err != nil {
+ view.Set("errorMessage", err.Error())
+ html.OK(w, view.Render("settings"))
+ return
+ }
+
+ if c.store.AnotherUserExists(user.ID, settingsForm.Username) {
+ view.Set("errorMessage", "This user already exists.")
+ html.OK(w, view.Render("settings"))
+ return
+ }
+
+ err = c.store.UpdateUser(settingsForm.Merge(user))
+ if err != nil {
+ logger.Error("[Controller:UpdateSettings] %v", err)
+ view.Set("errorMessage", "Unable to update this user.")
+ html.OK(w, view.Render("settings"))
+ return
+ }
+
+ sess.NewFlashMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("Preferences saved!"))
+ response.Redirect(w, r, route.Path(c.router, "settings"))
+}
diff --git a/ui/starred.go b/ui/starred.go
deleted file mode 100644
index 3ebd359..0000000
--- a/ui/starred.go
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package ui
-
-import (
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
-)
-
-// ShowStarredPage renders the page with all starred entries.
-func (c *Controller) ShowStarredPage(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
- offset := request.QueryIntegerParam("offset", 0)
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithoutStatus(model.EntryStatusRemoved)
- builder.WithStarred()
- builder.WithOrder(model.DefaultSortingOrder)
- builder.WithDirection(user.EntryDirection)
- builder.WithOffset(offset)
- builder.WithLimit(nbItemsPerPage)
-
- entries, err := builder.GetEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- count, err := builder.CountEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("starred", ctx.UserLanguage(), args.Merge(tplParams{
- "entries": entries,
- "total": count,
- "pagination": c.getPagination(ctx.Route("starred"), count, offset),
- "menu": "starred",
- }))
-}
-
-// ToggleBookmark handles Ajax request to toggle bookmark value.
-func (c *Controller) ToggleBookmark(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- if err := c.store.ToggleBookmark(user.ID, entryID); err != nil {
- logger.Error("[Controller:UpdateEntryStatus] %v", err)
- response.JSON().ServerError(nil)
- return
- }
-
- response.JSON().Standard("OK")
-}
diff --git a/ui/static.go b/ui/static.go
deleted file mode 100644
index 266d9e6..0000000
--- a/ui/static.go
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package ui
-
-import (
- "encoding/base64"
- "time"
-
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/ui/static"
-)
-
-// Stylesheet renders the CSS.
-func (c *Controller) Stylesheet(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- stylesheet := request.StringParam("name", "white")
- body := static.Stylesheets["common"]
- etag := static.StylesheetsChecksums["common"]
-
- if theme, found := static.Stylesheets[stylesheet]; found {
- body += theme
- etag += static.StylesheetsChecksums[stylesheet]
- }
-
- response.Cache("text/css; charset=utf-8", etag, []byte(body), 48*time.Hour)
-}
-
-// Javascript renders application client side code.
-func (c *Controller) Javascript(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- response.Cache("text/javascript; charset=utf-8", static.JavascriptChecksums["app"], []byte(static.Javascript["app"]), 48*time.Hour)
-}
-
-// Favicon renders the application favicon.
-func (c *Controller) Favicon(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- blob, err := base64.StdEncoding.DecodeString(static.Binaries["favicon.ico"])
- if err != nil {
- logger.Error("[Controller:Favicon] %v", err)
- response.HTML().NotFound()
- return
- }
-
- response.Cache("image/x-icon", static.BinariesChecksums["favicon.ico"], blob, 48*time.Hour)
-}
-
-// AppIcon returns application icons.
-func (c *Controller) AppIcon(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- filename := request.StringParam("filename", "favicon.png")
- encodedBlob, found := static.Binaries[filename]
- if !found {
- logger.Info("[Controller:AppIcon] This icon doesn't exists: %s", filename)
- response.HTML().NotFound()
- return
- }
-
- blob, err := base64.StdEncoding.DecodeString(encodedBlob)
- if err != nil {
- logger.Error("[Controller:AppIcon] %v", err)
- response.HTML().NotFound()
- return
- }
-
- response.Cache("image/png", static.BinariesChecksums[filename], blob, 48*time.Hour)
-}
-
-// WebManifest renders web manifest file.
-func (c *Controller) WebManifest(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- type webManifestIcon struct {
- Source string `json:"src"`
- Sizes string `json:"sizes"`
- Type string `json:"type"`
- }
-
- type webManifest struct {
- Name string `json:"name"`
- Description string `json:"description"`
- ShortName string `json:"short_name"`
- StartURL string `json:"start_url"`
- Icons []webManifestIcon `json:"icons"`
- Display string `json:"display"`
- }
-
- manifest := &webManifest{
- Name: "Miniflux",
- ShortName: "Miniflux",
- Description: "Minimalist Feed Reader",
- Display: "minimal-ui",
- StartURL: ctx.Route("unread"),
- Icons: []webManifestIcon{
- webManifestIcon{Source: ctx.Route("appIcon", "filename", "touch-icon-ipad-retina.png"), Sizes: "144x144", Type: "image/png"},
- webManifestIcon{Source: ctx.Route("appIcon", "filename", "touch-icon-iphone-retina.png"), Sizes: "114x114", Type: "image/png"},
- },
- }
-
- response.JSON().Standard(manifest)
-}
diff --git a/ui/static_app_icon.go b/ui/static_app_icon.go
new file mode 100644
index 0000000..19a823e
--- /dev/null
+++ b/ui/static_app_icon.go
@@ -0,0 +1,37 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "encoding/base64"
+ "net/http"
+ "time"
+
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/ui/static"
+)
+
+// AppIcon renders application icons.
+func (c *Controller) AppIcon(w http.ResponseWriter, r *http.Request) {
+ filename := request.Param(r, "filename", "favicon.png")
+ encodedBlob, found := static.Binaries[filename]
+ if !found {
+ logger.Info("[Controller:AppIcon] This icon doesn't exists: %s", filename)
+ html.NotFound(w)
+ return
+ }
+
+ blob, err := base64.StdEncoding.DecodeString(encodedBlob)
+ if err != nil {
+ logger.Error("[Controller:AppIcon] %v", err)
+ html.NotFound(w)
+ return
+ }
+
+ response.Cache(w, r, "image/png", static.BinariesChecksums[filename], blob, 48*time.Hour)
+}
diff --git a/ui/static_favicon.go b/ui/static_favicon.go
new file mode 100644
index 0000000..c68c556
--- /dev/null
+++ b/ui/static_favicon.go
@@ -0,0 +1,28 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "encoding/base64"
+ "net/http"
+ "time"
+
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/ui/static"
+)
+
+// Favicon renders the application favicon.
+func (c *Controller) Favicon(w http.ResponseWriter, r *http.Request) {
+ blob, err := base64.StdEncoding.DecodeString(static.Binaries["favicon.ico"])
+ if err != nil {
+ logger.Error("[Controller:Favicon] %v", err)
+ html.NotFound(w)
+ return
+ }
+
+ response.Cache(w, r, "image/x-icon", static.BinariesChecksums["favicon.ico"], blob, 48*time.Hour)
+}
diff --git a/ui/static_javascript.go b/ui/static_javascript.go
new file mode 100644
index 0000000..821a339
--- /dev/null
+++ b/ui/static_javascript.go
@@ -0,0 +1,18 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/ui/static"
+)
+
+// Javascript renders application client side code.
+func (c *Controller) Javascript(w http.ResponseWriter, r *http.Request) {
+ response.Cache(w, r, "text/javascript; charset=utf-8", static.JavascriptChecksums["app"], []byte(static.Javascript["app"]), 48*time.Hour)
+}
diff --git a/ui/static_manifest.go b/ui/static_manifest.go
new file mode 100644
index 0000000..8721718
--- /dev/null
+++ b/ui/static_manifest.go
@@ -0,0 +1,44 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/response/json"
+ "github.com/miniflux/miniflux/http/route"
+)
+
+// WebManifest renders web manifest file.
+func (c *Controller) WebManifest(w http.ResponseWriter, r *http.Request) {
+ type webManifestIcon struct {
+ Source string `json:"src"`
+ Sizes string `json:"sizes"`
+ Type string `json:"type"`
+ }
+
+ type webManifest struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ ShortName string `json:"short_name"`
+ StartURL string `json:"start_url"`
+ Icons []webManifestIcon `json:"icons"`
+ Display string `json:"display"`
+ }
+
+ manifest := &webManifest{
+ Name: "Miniflux",
+ ShortName: "Miniflux",
+ Description: "Minimalist Feed Reader",
+ Display: "minimal-ui",
+ StartURL: route.Path(c.router, "unread"),
+ Icons: []webManifestIcon{
+ webManifestIcon{Source: route.Path(c.router, "appIcon", "filename", "touch-icon-ipad-retina.png"), Sizes: "144x144", Type: "image/png"},
+ webManifestIcon{Source: route.Path(c.router, "appIcon", "filename", "touch-icon-iphone-retina.png"), Sizes: "114x114", Type: "image/png"},
+ },
+ }
+
+ json.OK(w, manifest)
+}
diff --git a/ui/static_stylesheet.go b/ui/static_stylesheet.go
new file mode 100644
index 0000000..2c58ba5
--- /dev/null
+++ b/ui/static_stylesheet.go
@@ -0,0 +1,28 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/ui/static"
+)
+
+// Stylesheet renders the CSS.
+func (c *Controller) Stylesheet(w http.ResponseWriter, r *http.Request) {
+ stylesheet := request.Param(r, "name", "white")
+ body := static.Stylesheets["common"]
+ etag := static.StylesheetsChecksums["common"]
+
+ if theme, found := static.Stylesheets[stylesheet]; found {
+ body += theme
+ etag += static.StylesheetsChecksums[stylesheet]
+ }
+
+ response.Cache(w, r, "text/css; charset=utf-8", etag, []byte(body), 48*time.Hour)
+}
diff --git a/ui/subscription.go b/ui/subscription.go
deleted file mode 100644
index e31ab60..0000000
--- a/ui/subscription.go
+++ /dev/null
@@ -1,145 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package ui
-
-import (
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/reader/subscription"
- "github.com/miniflux/miniflux/ui/form"
-)
-
-// Bookmarklet prefill the form to add a subscription from the URL provided by the bookmarklet.
-func (c *Controller) Bookmarklet(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
- args, err := c.getSubscriptionFormTemplateArgs(ctx, user)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- bookmarkletURL := request.QueryStringParam("uri", "")
- response.HTML().Render("add_subscription", ctx.UserLanguage(), args.Merge(tplParams{
- "form": &form.SubscriptionForm{URL: bookmarkletURL},
- }))
-}
-
-// AddSubscription shows the form to add a new feed.
-func (c *Controller) AddSubscription(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- args, err := c.getSubscriptionFormTemplateArgs(ctx, user)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("add_subscription", ctx.UserLanguage(), args)
-}
-
-// SubmitSubscription try to find a feed from the URL provided by the user.
-func (c *Controller) SubmitSubscription(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- args, err := c.getSubscriptionFormTemplateArgs(ctx, user)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- subscriptionForm := form.NewSubscriptionForm(request.Request())
- if err := subscriptionForm.Validate(); err != nil {
- response.HTML().Render("add_subscription", ctx.UserLanguage(), args.Merge(tplParams{
- "form": subscriptionForm,
- "errorMessage": err.Error(),
- }))
- return
- }
-
- subscriptions, err := subscription.FindSubscriptions(subscriptionForm.URL)
- if err != nil {
- logger.Error("[Controller:SubmitSubscription] %v", err)
- response.HTML().Render("add_subscription", ctx.UserLanguage(), args.Merge(tplParams{
- "form": subscriptionForm,
- "errorMessage": err,
- }))
- return
- }
-
- logger.Debug("[UI:SubmitSubscription] %s", subscriptions)
-
- n := len(subscriptions)
- switch {
- case n == 0:
- response.HTML().Render("add_subscription", ctx.UserLanguage(), args.Merge(tplParams{
- "form": subscriptionForm,
- "errorMessage": "Unable to find any subscription.",
- }))
- case n == 1:
- feed, err := c.feedHandler.CreateFeed(user.ID, subscriptionForm.CategoryID, subscriptions[0].URL, subscriptionForm.Crawler)
- if err != nil {
- response.HTML().Render("add_subscription", ctx.UserLanguage(), args.Merge(tplParams{
- "form": subscriptionForm,
- "errorMessage": err,
- }))
- return
- }
-
- response.Redirect(ctx.Route("feedEntries", "feedID", feed.ID))
- case n > 1:
- response.HTML().Render("choose_subscription", ctx.UserLanguage(), args.Merge(tplParams{
- "categoryID": subscriptionForm.CategoryID,
- "subscriptions": subscriptions,
- }))
- }
-}
-
-// ChooseSubscription shows a page to choose a subscription.
-func (c *Controller) ChooseSubscription(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- args, err := c.getSubscriptionFormTemplateArgs(ctx, user)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- subscriptionForm := form.NewSubscriptionForm(request.Request())
- if err := subscriptionForm.Validate(); err != nil {
- response.HTML().Render("add_subscription", ctx.UserLanguage(), args.Merge(tplParams{
- "form": subscriptionForm,
- "errorMessage": err.Error(),
- }))
- return
- }
-
- feed, err := c.feedHandler.CreateFeed(user.ID, subscriptionForm.CategoryID, subscriptionForm.URL, subscriptionForm.Crawler)
- if err != nil {
- response.HTML().Render("add_subscription", ctx.UserLanguage(), args.Merge(tplParams{
- "form": subscriptionForm,
- "errorMessage": err,
- }))
- return
- }
-
- response.Redirect(ctx.Route("feedEntries", "feedID", feed.ID))
-}
-
-func (c *Controller) getSubscriptionFormTemplateArgs(ctx *handler.Context, user *model.User) (tplParams, error) {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- return nil, err
- }
-
- categories, err := c.store.Categories(user.ID)
- if err != nil {
- return nil, err
- }
-
- args["categories"] = categories
- args["menu"] = "feeds"
- return args, nil
-}
diff --git a/ui/subscription_add.go b/ui/subscription_add.go
new file mode 100644
index 0000000..301c5c9
--- /dev/null
+++ b/ui/subscription_add.go
@@ -0,0 +1,40 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// AddSubscription shows the form to add a new feed.
+func (c *Controller) AddSubscription(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ categories, err := c.store.Categories(user.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ view.Set("categories", categories)
+ view.Set("menu", "feeds")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("add_subscription"))
+}
diff --git a/ui/subscription_bookmarklet.go b/ui/subscription_bookmarklet.go
new file mode 100644
index 0000000..4f31fa8
--- /dev/null
+++ b/ui/subscription_bookmarklet.go
@@ -0,0 +1,45 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// Bookmarklet prefill the form to add a subscription from the URL provided by the bookmarklet.
+func (c *Controller) Bookmarklet(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ categories, err := c.store.Categories(user.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ bookmarkletURL := request.QueryParam(r, "uri", "")
+
+ view.Set("form", form.SubscriptionForm{URL: bookmarkletURL})
+ view.Set("categories", categories)
+ view.Set("menu", "feeds")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("add_subscription"))
+}
diff --git a/ui/subscription_choose.go b/ui/subscription_choose.go
new file mode 100644
index 0000000..be97441
--- /dev/null
+++ b/ui/subscription_choose.go
@@ -0,0 +1,59 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// ChooseSubscription shows a page to choose a subscription.
+func (c *Controller) ChooseSubscription(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ categories, err := c.store.Categories(user.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ view.Set("categories", categories)
+ view.Set("menu", "feeds")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ subscriptionForm := form.NewSubscriptionForm(r)
+ if err := subscriptionForm.Validate(); err != nil {
+ view.Set("form", subscriptionForm)
+ view.Set("errorMessage", err.Error())
+ html.OK(w, view.Render("add_subscription"))
+ return
+ }
+
+ feed, err := c.feedHandler.CreateFeed(user.ID, subscriptionForm.CategoryID, subscriptionForm.URL, subscriptionForm.Crawler)
+ if err != nil {
+ view.Set("form", subscriptionForm)
+ view.Set("errorMessage", err)
+ html.OK(w, view.Render("add_subscription"))
+ return
+ }
+
+ response.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID))
+}
diff --git a/ui/subscription_submit.go b/ui/subscription_submit.go
new file mode 100644
index 0000000..f4767a2
--- /dev/null
+++ b/ui/subscription_submit.go
@@ -0,0 +1,89 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/reader/subscription"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// SubmitSubscription try to find a feed from the URL provided by the user.
+func (c *Controller) SubmitSubscription(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+ v := view.New(c.tpl, ctx, sess)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ categories, err := c.store.Categories(user.ID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ v.Set("categories", categories)
+ v.Set("menu", "feeds")
+ v.Set("user", user)
+ v.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ subscriptionForm := form.NewSubscriptionForm(r)
+ if err := subscriptionForm.Validate(); err != nil {
+ v.Set("form", subscriptionForm)
+ v.Set("errorMessage", err.Error())
+ html.OK(w, v.Render("add_subscription"))
+ return
+ }
+
+ subscriptions, err := subscription.FindSubscriptions(subscriptionForm.URL)
+ if err != nil {
+ logger.Error("[Controller:SubmitSubscription] %v", err)
+ v.Set("form", subscriptionForm)
+ v.Set("errorMessage", err)
+ html.OK(w, v.Render("add_subscription"))
+ return
+ }
+
+ logger.Debug("[UI:SubmitSubscription] %s", subscriptions)
+
+ n := len(subscriptions)
+ switch {
+ case n == 0:
+ v.Set("form", subscriptionForm)
+ v.Set("errorMessage", "Unable to find any subscription.")
+ html.OK(w, v.Render("add_subscription"))
+ case n == 1:
+ feed, err := c.feedHandler.CreateFeed(user.ID, subscriptionForm.CategoryID, subscriptions[0].URL, subscriptionForm.Crawler)
+ if err != nil {
+ v.Set("form", subscriptionForm)
+ v.Set("errorMessage", err)
+ html.OK(w, v.Render("add_subscription"))
+ return
+ }
+
+ response.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID))
+ case n > 1:
+ v := view.New(c.tpl, ctx, sess)
+ v.Set("subscriptions", subscriptions)
+ v.Set("categoryID", subscriptionForm.CategoryID)
+ v.Set("menu", "feeds")
+ v.Set("user", user)
+ v.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, v.Render("choose_subscription"))
+ }
+}
diff --git a/ui/unread.go b/ui/unread.go
deleted file mode 100644
index f38a12f..0000000
--- a/ui/unread.go
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package ui
-
-import (
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
-)
-
-// ShowUnreadPage render the page with all unread entries.
-func (c *Controller) ShowUnreadPage(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
- offset := request.QueryIntegerParam("offset", 0)
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithStatus(model.EntryStatusUnread)
- countUnread, err := builder.CountEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if offset >= countUnread {
- offset = 0
- }
-
- builder = c.store.NewEntryQueryBuilder(user.ID)
- builder.WithStatus(model.EntryStatusUnread)
- builder.WithOrder(model.DefaultSortingOrder)
- builder.WithDirection(user.EntryDirection)
- builder.WithOffset(offset)
- builder.WithLimit(nbItemsPerPage)
- entries, err := builder.GetEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("unread", ctx.UserLanguage(), tplParams{
- "user": user,
- "countUnread": countUnread,
- "entries": entries,
- "pagination": c.getPagination(ctx.Route("unread"), countUnread, offset),
- "menu": "unread",
- "csrf": ctx.CSRF(),
- })
-}
-
-// MarkAllAsRead marks all unread entries as read.
-func (c *Controller) MarkAllAsRead(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- if err := c.store.MarkAllAsRead(ctx.UserID()); err != nil {
- logger.Error("[MarkAllAsRead] %v", err)
- }
-
- response.Redirect(ctx.Route("unread"))
-}
diff --git a/ui/unread_entries.go b/ui/unread_entries.go
new file mode 100644
index 0000000..2ae47a7
--- /dev/null
+++ b/ui/unread_entries.go
@@ -0,0 +1,63 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowUnreadPage render the page with all unread entries.
+func (c *Controller) ShowUnreadPage(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ offset := request.QueryIntParam(r, "offset", 0)
+ builder := c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithStatus(model.EntryStatusUnread)
+ countUnread, err := builder.CountEntries()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if offset >= countUnread {
+ offset = 0
+ }
+
+ builder = c.store.NewEntryQueryBuilder(user.ID)
+ builder.WithStatus(model.EntryStatusUnread)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(user.EntryDirection)
+ builder.WithOffset(offset)
+ builder.WithLimit(nbItemsPerPage)
+ entries, err := builder.GetEntries()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ view.Set("entries", entries)
+ view.Set("pagination", c.getPagination(route.Path(c.router, "unread"), countUnread, offset))
+ view.Set("menu", "unread")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("unread"))
+}
diff --git a/ui/unread_mark_all_read.go b/ui/unread_mark_all_read.go
new file mode 100644
index 0000000..2c745e4
--- /dev/null
+++ b/ui/unread_mark_all_read.go
@@ -0,0 +1,23 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+)
+
+// MarkAllAsRead marks all unread entries as read.
+func (c *Controller) MarkAllAsRead(w http.ResponseWriter, r *http.Request) {
+ if err := c.store.MarkAllAsRead(context.New(r).UserID()); err != nil {
+ logger.Error("[MarkAllAsRead] %v", err)
+ }
+
+ response.Redirect(w, r, route.Path(c.router, "unread"))
+}
diff --git a/ui/user.go b/ui/user.go
deleted file mode 100644
index b607a85..0000000
--- a/ui/user.go
+++ /dev/null
@@ -1,239 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package ui
-
-import (
- "errors"
-
- "github.com/miniflux/miniflux/http/handler"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/ui/form"
-)
-
-// ShowUsers shows the list of users.
-func (c *Controller) ShowUsers(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- if !user.IsAdmin {
- response.HTML().Forbidden()
- return
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- users, err := c.store.Users()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- users.UseTimezone(user.Timezone)
- response.HTML().Render("users", ctx.UserLanguage(), args.Merge(tplParams{
- "users": users,
- "menu": "settings",
- }))
-}
-
-// CreateUser shows the user creation form.
-func (c *Controller) CreateUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- if !user.IsAdmin {
- response.HTML().Forbidden()
- return
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("create_user", ctx.UserLanguage(), args.Merge(tplParams{
- "menu": "settings",
- "form": &form.UserForm{},
- }))
-}
-
-// SaveUser validate and save the new user into the database.
-func (c *Controller) SaveUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- if !user.IsAdmin {
- response.HTML().Forbidden()
- return
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- userForm := form.NewUserForm(request.Request())
- if err := userForm.ValidateCreation(); err != nil {
- response.HTML().Render("create_user", ctx.UserLanguage(), args.Merge(tplParams{
- "menu": "settings",
- "form": userForm,
- "errorMessage": err.Error(),
- }))
- return
- }
-
- if c.store.UserExists(userForm.Username) {
- response.HTML().Render("create_user", ctx.UserLanguage(), args.Merge(tplParams{
- "menu": "settings",
- "form": userForm,
- "errorMessage": "This user already exists.",
- }))
- return
- }
-
- newUser := userForm.ToUser()
- if err := c.store.CreateUser(newUser); err != nil {
- logger.Error("[Controller:SaveUser] %v", err)
- response.HTML().Render("edit_user", ctx.UserLanguage(), args.Merge(tplParams{
- "menu": "settings",
- "form": userForm,
- "errorMessage": "Unable to create this user.",
- }))
- return
- }
-
- response.Redirect(ctx.Route("users"))
-}
-
-// EditUser shows the form to edit a user.
-func (c *Controller) EditUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- if !user.IsAdmin {
- response.HTML().Forbidden()
- return
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- selectedUser, err := c.getUserFromURL(ctx, request, response)
- if err != nil {
- return
- }
-
- response.HTML().Render("edit_user", ctx.UserLanguage(), args.Merge(tplParams{
- "menu": "settings",
- "selected_user": selectedUser,
- "form": &form.UserForm{
- Username: selectedUser.Username,
- IsAdmin: selectedUser.IsAdmin,
- },
- }))
-}
-
-// UpdateUser validate and update a user.
-func (c *Controller) UpdateUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
-
- if !user.IsAdmin {
- response.HTML().Forbidden()
- return
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- selectedUser, err := c.getUserFromURL(ctx, request, response)
- if err != nil {
- return
- }
-
- userForm := form.NewUserForm(request.Request())
- if err := userForm.ValidateModification(); err != nil {
- response.HTML().Render("edit_user", ctx.UserLanguage(), args.Merge(tplParams{
- "menu": "settings",
- "selected_user": selectedUser,
- "form": userForm,
- "errorMessage": err.Error(),
- }))
- return
- }
-
- if c.store.AnotherUserExists(selectedUser.ID, userForm.Username) {
- response.HTML().Render("edit_user", ctx.UserLanguage(), args.Merge(tplParams{
- "menu": "settings",
- "selected_user": selectedUser,
- "form": userForm,
- "errorMessage": "This user already exists.",
- }))
- return
- }
-
- userForm.Merge(selectedUser)
- if err := c.store.UpdateUser(selectedUser); err != nil {
- logger.Error("[Controller:UpdateUser] %v", err)
- response.HTML().Render("edit_user", ctx.UserLanguage(), args.Merge(tplParams{
- "menu": "settings",
- "selected_user": selectedUser,
- "form": userForm,
- "errorMessage": "Unable to update this user.",
- }))
- return
- }
-
- response.Redirect(ctx.Route("users"))
-}
-
-// RemoveUser deletes a user from the database.
-func (c *Controller) RemoveUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
- user := ctx.LoggedUser()
- if !user.IsAdmin {
- response.HTML().Forbidden()
- return
- }
-
- selectedUser, err := c.getUserFromURL(ctx, request, response)
- if err != nil {
- return
- }
-
- if err := c.store.RemoveUser(selectedUser.ID); err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.Redirect(ctx.Route("users"))
-}
-
-func (c *Controller) getUserFromURL(ctx *handler.Context, request *handler.Request, response *handler.Response) (*model.User, error) {
- userID, err := request.IntegerParam("userID")
- if err != nil {
- response.HTML().BadRequest(err)
- return nil, err
- }
-
- user, err := c.store.UserByID(userID)
- if err != nil {
- response.HTML().ServerError(err)
- return nil, err
- }
-
- if user == nil {
- response.HTML().NotFound()
- return nil, errors.New("User not found")
- }
-
- return user, nil
-}
diff --git a/ui/user_create.go b/ui/user_create.go
new file mode 100644
index 0000000..4835863
--- /dev/null
+++ b/ui/user_create.go
@@ -0,0 +1,40 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// CreateUser shows the user creation form.
+func (c *Controller) CreateUser(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if !user.IsAdmin {
+ html.Forbidden(w)
+ return
+ }
+
+ view.Set("form", &form.UserForm{})
+ view.Set("menu", "settings")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("create_user"))
+}
diff --git a/ui/user_edit.go b/ui/user_edit.go
new file mode 100644
index 0000000..648e4d6
--- /dev/null
+++ b/ui/user_edit.go
@@ -0,0 +1,64 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// EditUser shows the form to edit a user.
+func (c *Controller) EditUser(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if !user.IsAdmin {
+ html.Forbidden(w)
+ return
+ }
+
+ userID, err := request.IntParam(r, "userID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ selectedUser, err := c.store.UserByID(userID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if selectedUser == nil {
+ html.NotFound(w)
+ return
+ }
+
+ userForm := &form.UserForm{
+ Username: selectedUser.Username,
+ IsAdmin: selectedUser.IsAdmin,
+ }
+
+ view.Set("form", userForm)
+ view.Set("selected_user", selectedUser)
+ view.Set("menu", "settings")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("edit_user"))
+}
diff --git a/ui/user_list.go b/ui/user_list.go
new file mode 100644
index 0000000..4bab764
--- /dev/null
+++ b/ui/user_list.go
@@ -0,0 +1,47 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowUsers renders the list of users.
+func (c *Controller) ShowUsers(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if !user.IsAdmin {
+ html.Forbidden(w)
+ return
+ }
+
+ users, err := c.store.Users()
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ users.UseTimezone(user.Timezone)
+
+ view.Set("users", users)
+ view.Set("menu", "settings")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+
+ html.OK(w, view.Render("users"))
+}
diff --git a/ui/user_remove.go b/ui/user_remove.go
new file mode 100644
index 0000000..0b9113b
--- /dev/null
+++ b/ui/user_remove.go
@@ -0,0 +1,55 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+)
+
+// RemoveUser deletes a user from the database.
+func (c *Controller) RemoveUser(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if !user.IsAdmin {
+ html.Forbidden(w)
+ return
+ }
+
+ userID, err := request.IntParam(r, "userID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ selectedUser, err := c.store.UserByID(userID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if selectedUser == nil {
+ html.NotFound(w)
+ return
+ }
+
+ if err := c.store.RemoveUser(selectedUser.ID); err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ response.Redirect(w, r, route.Path(c.router, "users"))
+}
diff --git a/ui/user_save.go b/ui/user_save.go
new file mode 100644
index 0000000..a0057e1
--- /dev/null
+++ b/ui/user_save.go
@@ -0,0 +1,65 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// SaveUser validate and save the new user into the database.
+func (c *Controller) SaveUser(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if !user.IsAdmin {
+ html.Forbidden(w)
+ return
+ }
+
+ userForm := form.NewUserForm(r)
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("menu", "settings")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+ view.Set("form", userForm)
+
+ if err := userForm.ValidateCreation(); err != nil {
+ view.Set("errorMessage", err.Error())
+ html.OK(w, view.Render("create_user"))
+ return
+ }
+
+ if c.store.UserExists(userForm.Username) {
+ view.Set("errorMessage", "This user already exists.")
+ html.OK(w, view.Render("create_user"))
+ return
+ }
+
+ newUser := userForm.ToUser()
+ if err := c.store.CreateUser(newUser); err != nil {
+ logger.Error("[Controller:SaveUser] %v", err)
+ view.Set("errorMessage", "Unable to create this user.")
+ html.OK(w, view.Render("create_user"))
+ return
+ }
+
+ response.Redirect(w, r, route.Path(c.router, "users"))
+}
diff --git a/ui/user_update.go b/ui/user_update.go
new file mode 100644
index 0000000..d14f8c9
--- /dev/null
+++ b/ui/user_update.go
@@ -0,0 +1,84 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+ "net/http"
+
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/http/request"
+ "github.com/miniflux/miniflux/http/response"
+ "github.com/miniflux/miniflux/http/response/html"
+ "github.com/miniflux/miniflux/http/route"
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/ui/form"
+ "github.com/miniflux/miniflux/ui/session"
+ "github.com/miniflux/miniflux/ui/view"
+)
+
+// UpdateUser validate and update a user.
+func (c *Controller) UpdateUser(w http.ResponseWriter, r *http.Request) {
+ ctx := context.New(r)
+
+ user, err := c.store.UserByID(ctx.UserID())
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if !user.IsAdmin {
+ html.Forbidden(w)
+ return
+ }
+
+ userID, err := request.IntParam(r, "userID")
+ if err != nil {
+ html.BadRequest(w, err)
+ return
+ }
+
+ selectedUser, err := c.store.UserByID(userID)
+ if err != nil {
+ html.ServerError(w, err)
+ return
+ }
+
+ if selectedUser == nil {
+ html.NotFound(w)
+ return
+ }
+
+ userForm := form.NewUserForm(r)
+
+ sess := session.New(c.store, ctx)
+ view := view.New(c.tpl, ctx, sess)
+ view.Set("menu", "settings")
+ view.Set("user", user)
+ view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+ view.Set("selected_user", selectedUser)
+ view.Set("form", userForm)
+
+ if err := userForm.ValidateModification(); err != nil {
+ view.Set("errorMessage", err.Error())
+ html.OK(w, view.Render("edit_user"))
+ return
+ }
+
+ if c.store.AnotherUserExists(selectedUser.ID, userForm.Username) {
+ view.Set("errorMessage", "This user already exists.")
+ html.OK(w, view.Render("edit_user"))
+ return
+ }
+
+ userForm.Merge(selectedUser)
+ if err := c.store.UpdateUser(selectedUser); err != nil {
+ logger.Error("[Controller:UpdateUser] %v", err)
+ view.Set("errorMessage", "Unable to update this user.")
+ html.OK(w, view.Render("edit_user"))
+ return
+ }
+
+ response.Redirect(w, r, route.Path(c.router, "users"))
+}
diff --git a/ui/view/view.go b/ui/view/view.go
new file mode 100644
index 0000000..a1c6646
--- /dev/null
+++ b/ui/view/view.go
@@ -0,0 +1,39 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package view
+
+import (
+ "github.com/miniflux/miniflux/http/context"
+ "github.com/miniflux/miniflux/template"
+ "github.com/miniflux/miniflux/ui/session"
+)
+
+// View wraps template argument building.
+type View struct {
+ tpl *template.Engine
+ ctx *context.Context
+ params map[string]interface{}
+}
+
+// Set adds a new template argument.
+func (v *View) Set(param string, value interface{}) *View {
+ v.params[param] = value
+ return v
+}
+
+// Render executes the template with arguments.
+func (v *View) Render(template string) []byte {
+ return v.tpl.Render(template, v.ctx.UserLanguage(), v.params)
+}
+
+// New returns a new view with default parameters.
+func New(tpl *template.Engine, ctx *context.Context, sess *session.Session) *View {
+ b := &View{tpl, ctx, make(map[string]interface{})}
+ b.params["menu"] = ""
+ b.params["csrf"] = ctx.CSRF()
+ b.params["flashMessage"] = sess.FlashMessage()
+ b.params["flashErrorMessage"] = sess.FlashErrorMessage()
+ return b
+}