aboutsummaryrefslogtreecommitdiff
path: root/server/ui/controller
diff options
context:
space:
mode:
Diffstat (limited to 'server/ui/controller')
-rw-r--r--server/ui/controller/about.go24
-rw-r--r--server/ui/controller/category.go228
-rw-r--r--server/ui/controller/controller.go56
-rw-r--r--server/ui/controller/entry.go375
-rw-r--r--server/ui/controller/feed.go209
-rw-r--r--server/ui/controller/history.go47
-rw-r--r--server/ui/controller/icon.go31
-rw-r--r--server/ui/controller/login.go91
-rw-r--r--server/ui/controller/opml.go63
-rw-r--r--server/ui/controller/pagination.go46
-rw-r--r--server/ui/controller/proxy.go49
-rw-r--r--server/ui/controller/session.go49
-rw-r--r--server/ui/controller/settings.go92
-rw-r--r--server/ui/controller/static.go41
-rw-r--r--server/ui/controller/subscription.go127
-rw-r--r--server/ui/controller/unread.go43
-rw-r--r--server/ui/controller/user.go231
17 files changed, 1802 insertions, 0 deletions
diff --git a/server/ui/controller/about.go b/server/ui/controller/about.go
new file mode 100644
index 00000000..dcfe0d7a
--- /dev/null
+++ b/server/ui/controller/about.go
@@ -0,0 +1,24 @@
+// 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 controller
+
+import (
+ "github.com/miniflux/miniflux2/server/core"
+ "github.com/miniflux/miniflux2/version"
+)
+
+func (c *Controller) AboutPage(ctx *core.Context, request *core.Request, response *core.Response) {
+ args, err := c.getCommonTemplateArgs(ctx)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ response.Html().Render("about", args.Merge(tplParams{
+ "version": version.Version,
+ "build_date": version.BuildDate,
+ "menu": "settings",
+ }))
+}
diff --git a/server/ui/controller/category.go b/server/ui/controller/category.go
new file mode 100644
index 00000000..dbc80671
--- /dev/null
+++ b/server/ui/controller/category.go
@@ -0,0 +1,228 @@
+// 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 controller
+
+import (
+ "errors"
+ "github.com/miniflux/miniflux2/model"
+ "github.com/miniflux/miniflux2/server/core"
+ "github.com/miniflux/miniflux2/server/ui/form"
+ "log"
+)
+
+func (c *Controller) ShowCategories(ctx *core.Context, request *core.Request, response *core.Response) {
+ args, err := c.getCommonTemplateArgs(ctx)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ user := ctx.GetLoggedUser()
+ categories, err := c.store.GetCategoriesWithFeedCount(user.ID)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ response.Html().Render("categories", args.Merge(tplParams{
+ "categories": categories,
+ "total": len(categories),
+ "menu": "categories",
+ }))
+}
+
+func (c *Controller) ShowCategoryEntries(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+ offset := request.GetQueryIntegerParam("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.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithCategoryID(category.ID)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(model.DefaultSortingDirection)
+ builder.WithOffset(offset)
+ builder.WithLimit(NbItemsPerPage)
+
+ entries, err := builder.GetEntries()
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ count, err := builder.CountEntries()
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ response.Html().Render("category_entries", args.Merge(tplParams{
+ "category": category,
+ "entries": entries,
+ "total": count,
+ "pagination": c.getPagination(ctx.GetRoute("categoryEntries", "categoryID", category.ID), count, offset),
+ "menu": "categories",
+ }))
+}
+
+func (c *Controller) CreateCategory(ctx *core.Context, request *core.Request, response *core.Response) {
+ args, err := c.getCommonTemplateArgs(ctx)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ response.Html().Render("create_category", args.Merge(tplParams{
+ "menu": "categories",
+ }))
+}
+
+func (c *Controller) SaveCategory(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+ args, err := c.getCommonTemplateArgs(ctx)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ categoryForm := form.NewCategoryForm(request.GetRequest())
+ if err := categoryForm.Validate(); err != nil {
+ response.Html().Render("create_category", args.Merge(tplParams{
+ "errorMessage": err.Error(),
+ }))
+ return
+ }
+
+ category := model.Category{Title: categoryForm.Title, UserID: user.ID}
+ err = c.store.CreateCategory(&category)
+ if err != nil {
+ log.Println(err)
+ response.Html().Render("create_category", args.Merge(tplParams{
+ "errorMessage": "Unable to create this category.",
+ }))
+ return
+ }
+
+ response.Redirect(ctx.GetRoute("categories"))
+}
+
+func (c *Controller) EditCategory(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ category, err := c.getCategoryFromURL(ctx, request, response)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+
+ args, err := c.getCategoryFormTemplateArgs(ctx, user, category, nil)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ response.Html().Render("edit_category", args)
+}
+
+func (c *Controller) UpdateCategory(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ category, err := c.getCategoryFromURL(ctx, request, response)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+
+ categoryForm := form.NewCategoryForm(request.GetRequest())
+ args, err := c.getCategoryFormTemplateArgs(ctx, user, category, categoryForm)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ if err := categoryForm.Validate(); err != nil {
+ response.Html().Render("edit_category", args.Merge(tplParams{
+ "errorMessage": err.Error(),
+ }))
+ return
+ }
+
+ err = c.store.UpdateCategory(categoryForm.Merge(category))
+ if err != nil {
+ log.Println(err)
+ response.Html().Render("edit_category", args.Merge(tplParams{
+ "errorMessage": "Unable to update this category.",
+ }))
+ return
+ }
+
+ response.Redirect(ctx.GetRoute("categories"))
+}
+
+func (c *Controller) RemoveCategory(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ 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.GetRoute("categories"))
+}
+
+func (c *Controller) getCategoryFromURL(ctx *core.Context, request *core.Request, response *core.Response) (*model.Category, error) {
+ categoryID, err := request.GetIntegerParam("categoryID")
+ if err != nil {
+ response.Html().BadRequest(err)
+ return nil, err
+ }
+
+ user := ctx.GetLoggedUser()
+ category, err := c.store.GetCategory(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 *core.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/server/ui/controller/controller.go b/server/ui/controller/controller.go
new file mode 100644
index 00000000..aad32582
--- /dev/null
+++ b/server/ui/controller/controller.go
@@ -0,0 +1,56 @@
+// 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 controller
+
+import (
+ "github.com/miniflux/miniflux2/model"
+ "github.com/miniflux/miniflux2/reader/feed"
+ "github.com/miniflux/miniflux2/reader/opml"
+ "github.com/miniflux/miniflux2/server/core"
+ "github.com/miniflux/miniflux2/storage"
+)
+
+type tplParams map[string]interface{}
+
+func (t tplParams) Merge(d tplParams) tplParams {
+ for k, v := range d {
+ t[k] = v
+ }
+
+ return t
+}
+
+type Controller struct {
+ store *storage.Storage
+ feedHandler *feed.Handler
+ opmlHandler *opml.OpmlHandler
+}
+
+func (c *Controller) getCommonTemplateArgs(ctx *core.Context) (tplParams, error) {
+ user := ctx.GetLoggedUser()
+ builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithStatus(model.EntryStatusUnread)
+
+ countUnread, err := builder.CountEntries()
+ if err != nil {
+ return nil, err
+ }
+
+ params := tplParams{
+ "menu": "",
+ "user": user,
+ "countUnread": countUnread,
+ "csrf": ctx.GetCsrfToken(),
+ }
+ return params, nil
+}
+
+func NewController(store *storage.Storage, feedHandler *feed.Handler, opmlHandler *opml.OpmlHandler) *Controller {
+ return &Controller{
+ store: store,
+ feedHandler: feedHandler,
+ opmlHandler: opmlHandler,
+ }
+}
diff --git a/server/ui/controller/entry.go b/server/ui/controller/entry.go
new file mode 100644
index 00000000..5a3a979c
--- /dev/null
+++ b/server/ui/controller/entry.go
@@ -0,0 +1,375 @@
+// 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 controller
+
+import (
+ "errors"
+ "github.com/miniflux/miniflux2/model"
+ "github.com/miniflux/miniflux2/server/core"
+ "github.com/miniflux/miniflux2/server/ui/payload"
+ "log"
+)
+
+func (c *Controller) ShowFeedEntry(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+ sortingDirection := model.DefaultSortingDirection
+
+ entryID, err := request.GetIntegerParam("entryID")
+ if err != nil {
+ response.Html().BadRequest(err)
+ return
+ }
+
+ feedID, err := request.GetIntegerParam("feedID")
+ if err != nil {
+ response.Html().BadRequest(err)
+ return
+ }
+
+ builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithFeedID(feedID)
+ builder.WithEntryID(entryID)
+
+ 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.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithFeedID(feedID)
+ builder.WithCondition("e.id", "!=", entryID)
+ builder.WithCondition("e.published_at", "<=", entry.Date)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(model.DefaultSortingDirection)
+ nextEntry, err := builder.GetEntry()
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithFeedID(feedID)
+ builder.WithCondition("e.id", "!=", entryID)
+ builder.WithCondition("e.published_at", ">=", entry.Date)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(model.GetOppositeDirection(sortingDirection))
+ prevEntry, err := builder.GetEntry()
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ nextEntryRoute := ""
+ if nextEntry != nil {
+ nextEntryRoute = ctx.GetRoute("feedEntry", "feedID", feedID, "entryID", nextEntry.ID)
+ }
+
+ prevEntryRoute := ""
+ if prevEntry != nil {
+ prevEntryRoute = ctx.GetRoute("feedEntry", "feedID", feedID, "entryID", prevEntry.ID)
+ }
+
+ if entry.Status == model.EntryStatusUnread {
+ err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+ if err != nil {
+ log.Println(err)
+ response.Html().ServerError(nil)
+ return
+ }
+ }
+
+ response.Html().Render("entry", args.Merge(tplParams{
+ "entry": entry,
+ "prevEntry": prevEntry,
+ "nextEntry": nextEntry,
+ "nextEntryRoute": nextEntryRoute,
+ "prevEntryRoute": prevEntryRoute,
+ "menu": "feeds",
+ }))
+}
+
+func (c *Controller) ShowCategoryEntry(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+ sortingDirection := model.DefaultSortingDirection
+
+ categoryID, err := request.GetIntegerParam("categoryID")
+ if err != nil {
+ response.Html().BadRequest(err)
+ return
+ }
+
+ entryID, err := request.GetIntegerParam("entryID")
+ if err != nil {
+ response.Html().BadRequest(err)
+ return
+ }
+
+ builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithCategoryID(categoryID)
+ builder.WithEntryID(entryID)
+
+ 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.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithCategoryID(categoryID)
+ builder.WithCondition("e.id", "!=", entryID)
+ builder.WithCondition("e.published_at", "<=", entry.Date)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(sortingDirection)
+ nextEntry, err := builder.GetEntry()
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithCategoryID(categoryID)
+ builder.WithCondition("e.id", "!=", entryID)
+ builder.WithCondition("e.published_at", ">=", entry.Date)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(model.GetOppositeDirection(sortingDirection))
+ prevEntry, err := builder.GetEntry()
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ nextEntryRoute := ""
+ if nextEntry != nil {
+ nextEntryRoute = ctx.GetRoute("categoryEntry", "categoryID", categoryID, "entryID", nextEntry.ID)
+ }
+
+ prevEntryRoute := ""
+ if prevEntry != nil {
+ prevEntryRoute = ctx.GetRoute("categoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID)
+ }
+
+ if entry.Status == model.EntryStatusUnread {
+ err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+ if err != nil {
+ log.Println(err)
+ response.Html().ServerError(nil)
+ return
+ }
+ }
+
+ response.Html().Render("entry", args.Merge(tplParams{
+ "entry": entry,
+ "prevEntry": prevEntry,
+ "nextEntry": nextEntry,
+ "nextEntryRoute": nextEntryRoute,
+ "prevEntryRoute": prevEntryRoute,
+ "menu": "categories",
+ }))
+}
+
+func (c *Controller) ShowUnreadEntry(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+ sortingDirection := model.DefaultSortingDirection
+
+ entryID, err := request.GetIntegerParam("entryID")
+ if err != nil {
+ response.Html().BadRequest(err)
+ return
+ }
+
+ builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithEntryID(entryID)
+
+ 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.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithStatus(model.EntryStatusUnread)
+ builder.WithCondition("e.id", "!=", entryID)
+ builder.WithCondition("e.published_at", "<=", entry.Date)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(sortingDirection)
+ nextEntry, err := builder.GetEntry()
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithStatus(model.EntryStatusUnread)
+ builder.WithCondition("e.id", "!=", entryID)
+ builder.WithCondition("e.published_at", ">=", entry.Date)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(model.GetOppositeDirection(sortingDirection))
+ prevEntry, err := builder.GetEntry()
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ nextEntryRoute := ""
+ if nextEntry != nil {
+ nextEntryRoute = ctx.GetRoute("unreadEntry", "entryID", nextEntry.ID)
+ }
+
+ prevEntryRoute := ""
+ if prevEntry != nil {
+ prevEntryRoute = ctx.GetRoute("unreadEntry", "entryID", prevEntry.ID)
+ }
+
+ if entry.Status == model.EntryStatusUnread {
+ err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+ if err != nil {
+ log.Println(err)
+ response.Html().ServerError(nil)
+ return
+ }
+ }
+
+ response.Html().Render("entry", args.Merge(tplParams{
+ "entry": entry,
+ "prevEntry": prevEntry,
+ "nextEntry": nextEntry,
+ "nextEntryRoute": nextEntryRoute,
+ "prevEntryRoute": prevEntryRoute,
+ "menu": "unread",
+ }))
+}
+
+func (c *Controller) ShowReadEntry(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+ sortingDirection := model.DefaultSortingDirection
+
+ entryID, err := request.GetIntegerParam("entryID")
+ if err != nil {
+ response.Html().BadRequest(err)
+ return
+ }
+
+ builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithEntryID(entryID)
+
+ 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.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithStatus(model.EntryStatusRead)
+ builder.WithCondition("e.id", "!=", entryID)
+ builder.WithCondition("e.published_at", "<=", entry.Date)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(sortingDirection)
+ nextEntry, err := builder.GetEntry()
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithStatus(model.EntryStatusRead)
+ builder.WithCondition("e.id", "!=", entryID)
+ builder.WithCondition("e.published_at", ">=", entry.Date)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(model.GetOppositeDirection(sortingDirection))
+ prevEntry, err := builder.GetEntry()
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ nextEntryRoute := ""
+ if nextEntry != nil {
+ nextEntryRoute = ctx.GetRoute("readEntry", "entryID", nextEntry.ID)
+ }
+
+ prevEntryRoute := ""
+ if prevEntry != nil {
+ prevEntryRoute = ctx.GetRoute("readEntry", "entryID", prevEntry.ID)
+ }
+
+ response.Html().Render("entry", args.Merge(tplParams{
+ "entry": entry,
+ "prevEntry": prevEntry,
+ "nextEntry": nextEntry,
+ "nextEntryRoute": nextEntryRoute,
+ "prevEntryRoute": prevEntryRoute,
+ "menu": "history",
+ }))
+}
+
+func (c *Controller) UpdateEntriesStatus(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ entryIDs, status, err := payload.DecodeEntryStatusPayload(request.GetBody())
+ if err != nil {
+ log.Println(err)
+ response.Json().BadRequest(nil)
+ return
+ }
+
+ if len(entryIDs) == 0 {
+ response.Html().BadRequest(errors.New("The list of entryID is empty"))
+ return
+ }
+
+ err = c.store.SetEntriesStatus(user.ID, entryIDs, status)
+ if err != nil {
+ log.Println(err)
+ response.Html().ServerError(nil)
+ return
+ }
+
+ response.Json().Standard("OK")
+}
diff --git a/server/ui/controller/feed.go b/server/ui/controller/feed.go
new file mode 100644
index 00000000..400f81ad
--- /dev/null
+++ b/server/ui/controller/feed.go
@@ -0,0 +1,209 @@
+// 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 controller
+
+import (
+ "errors"
+ "github.com/miniflux/miniflux2/model"
+ "github.com/miniflux/miniflux2/server/core"
+ "github.com/miniflux/miniflux2/server/ui/form"
+ "log"
+)
+
+func (c *Controller) ShowFeedsPage(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ args, err := c.getCommonTemplateArgs(ctx)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ feeds, err := c.store.GetFeeds(user.ID)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ response.Html().Render("feeds", args.Merge(tplParams{
+ "feeds": feeds,
+ "total": len(feeds),
+ "menu": "feeds",
+ }))
+}
+
+func (c *Controller) ShowFeedEntries(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+ offset := request.GetQueryIntegerParam("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.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithFeedID(feed.ID)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(model.DefaultSortingDirection)
+ 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", args.Merge(tplParams{
+ "feed": feed,
+ "entries": entries,
+ "total": count,
+ "pagination": c.getPagination(ctx.GetRoute("feedEntries", "feedID", feed.ID), count, offset),
+ "menu": "feeds",
+ }))
+}
+
+func (c *Controller) EditFeed(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ 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", args)
+}
+
+func (c *Controller) UpdateFeed(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ feed, err := c.getFeedFromURL(request, response, user)
+ if err != nil {
+ return
+ }
+
+ feedForm := form.NewFeedForm(request.GetRequest())
+ 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", args.Merge(tplParams{
+ "errorMessage": err.Error(),
+ }))
+ return
+ }
+
+ err = c.store.UpdateFeed(feedForm.Merge(feed))
+ if err != nil {
+ log.Println(err)
+ response.Html().Render("edit_feed", args.Merge(tplParams{
+ "errorMessage": "Unable to update this feed.",
+ }))
+ return
+ }
+
+ response.Redirect(ctx.GetRoute("feeds"))
+}
+
+func (c *Controller) RemoveFeed(ctx *core.Context, request *core.Request, response *core.Response) {
+ feedID, err := request.GetIntegerParam("feedID")
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ user := ctx.GetLoggedUser()
+ if err := c.store.RemoveFeed(user.ID, feedID); err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ response.Redirect(ctx.GetRoute("feeds"))
+}
+
+func (c *Controller) RefreshFeed(ctx *core.Context, request *core.Request, response *core.Response) {
+ feedID, err := request.GetIntegerParam("feedID")
+ if err != nil {
+ response.Html().BadRequest(err)
+ return
+ }
+
+ user := ctx.GetLoggedUser()
+ if err := c.feedHandler.RefreshFeed(user.ID, feedID); err != nil {
+ log.Println("[UI:RefreshFeed]", err)
+ }
+
+ response.Redirect(ctx.GetRoute("feedEntries", "feedID", feedID))
+}
+
+func (c *Controller) getFeedFromURL(request *core.Request, response *core.Response, user *model.User) (*model.Feed, error) {
+ feedID, err := request.GetIntegerParam("feedID")
+ if err != nil {
+ response.Html().BadRequest(err)
+ return nil, err
+ }
+
+ feed, err := c.store.GetFeedById(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 *core.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.GetCategories(user.ID)
+ if err != nil {
+ return nil, err
+ }
+
+ if feedForm == nil {
+ args["form"] = form.FeedForm{
+ SiteURL: feed.SiteURL,
+ FeedURL: feed.FeedURL,
+ Title: feed.Title,
+ CategoryID: feed.Category.ID,
+ }
+ } else {
+ args["form"] = feedForm
+ }
+
+ args["categories"] = categories
+ args["feed"] = feed
+ args["menu"] = "feeds"
+ return args, nil
+}
diff --git a/server/ui/controller/history.go b/server/ui/controller/history.go
new file mode 100644
index 00000000..2c067373
--- /dev/null
+++ b/server/ui/controller/history.go
@@ -0,0 +1,47 @@
+// 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 controller
+
+import (
+ "github.com/miniflux/miniflux2/model"
+ "github.com/miniflux/miniflux2/server/core"
+)
+
+func (c *Controller) ShowHistoryPage(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+ offset := request.GetQueryIntegerParam("offset", 0)
+
+ args, err := c.getCommonTemplateArgs(ctx)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithStatus(model.EntryStatusRead)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(model.DefaultSortingDirection)
+ 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", args.Merge(tplParams{
+ "entries": entries,
+ "total": count,
+ "pagination": c.getPagination(ctx.GetRoute("history"), count, offset),
+ "menu": "history",
+ }))
+}
diff --git a/server/ui/controller/icon.go b/server/ui/controller/icon.go
new file mode 100644
index 00000000..37954c24
--- /dev/null
+++ b/server/ui/controller/icon.go
@@ -0,0 +1,31 @@
+// 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 controller
+
+import (
+ "github.com/miniflux/miniflux2/server/core"
+ "time"
+)
+
+func (c *Controller) ShowIcon(ctx *core.Context, request *core.Request, response *core.Response) {
+ iconID, err := request.GetIntegerParam("iconID")
+ if err != nil {
+ response.Html().BadRequest(err)
+ return
+ }
+
+ icon, err := c.store.GetIconByID(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/server/ui/controller/login.go b/server/ui/controller/login.go
new file mode 100644
index 00000000..225978c1
--- /dev/null
+++ b/server/ui/controller/login.go
@@ -0,0 +1,91 @@
+// 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 controller
+
+import (
+ "github.com/miniflux/miniflux2/server/core"
+ "github.com/miniflux/miniflux2/server/ui/form"
+ "log"
+ "net/http"
+ "time"
+
+ "github.com/tomasen/realip"
+)
+
+func (c *Controller) ShowLoginPage(ctx *core.Context, request *core.Request, response *core.Response) {
+ if ctx.IsAuthenticated() {
+ response.Redirect(ctx.GetRoute("unread"))
+ return
+ }
+
+ response.Html().Render("login", tplParams{
+ "csrf": ctx.GetCsrfToken(),
+ })
+}
+
+func (c *Controller) CheckLogin(ctx *core.Context, request *core.Request, response *core.Response) {
+ authForm := form.NewAuthForm(request.GetRequest())
+ tplParams := tplParams{
+ "errorMessage": "Invalid username or password.",
+ "csrf": ctx.GetCsrfToken(),
+ }
+
+ if err := authForm.Validate(); err != nil {
+ log.Println(err)
+ response.Html().Render("login", tplParams)
+ return
+ }
+
+ if err := c.store.CheckPassword(authForm.Username, authForm.Password); err != nil {
+ log.Println(err)
+ response.Html().Render("login", tplParams)
+ return
+ }
+
+ sessionToken, err := c.store.CreateSession(
+ authForm.Username,
+ request.GetHeaders().Get("User-Agent"),
+ realip.RealIP(request.GetRequest()),
+ )
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ log.Printf("[UI:CheckLogin] username=%s just logged in\n", authForm.Username)
+
+ cookie := &http.Cookie{
+ Name: "sessionID",
+ Value: sessionToken,
+ Path: "/",
+ Secure: request.IsHTTPS(),
+ HttpOnly: true,
+ }
+
+ response.SetCookie(cookie)
+ response.Redirect(ctx.GetRoute("unread"))
+}
+
+func (c *Controller) Logout(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ sessionCookie := request.GetCookie("sessionID")
+ if err := c.store.RemoveSessionByToken(user.ID, sessionCookie); err != nil {
+ log.Printf("[UI:Logout] %v", err)
+ }
+
+ cookie := &http.Cookie{
+ Name: "sessionID",
+ Value: "",
+ Path: "/",
+ Secure: request.IsHTTPS(),
+ HttpOnly: true,
+ MaxAge: -1,
+ Expires: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
+ }
+
+ response.SetCookie(cookie)
+ response.Redirect(ctx.GetRoute("login"))
+}
diff --git a/server/ui/controller/opml.go b/server/ui/controller/opml.go
new file mode 100644
index 00000000..45d34f8e
--- /dev/null
+++ b/server/ui/controller/opml.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 controller
+
+import (
+ "github.com/miniflux/miniflux2/server/core"
+ "log"
+)
+
+func (c *Controller) Export(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+ opml, err := c.opmlHandler.Export(user.ID)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ response.Xml().Download("feeds.opml", opml)
+}
+
+func (c *Controller) Import(ctx *core.Context, request *core.Request, response *core.Response) {
+ args, err := c.getCommonTemplateArgs(ctx)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ response.Html().Render("import", args.Merge(tplParams{
+ "menu": "feeds",
+ }))
+}
+
+func (c *Controller) UploadOPML(ctx *core.Context, request *core.Request, response *core.Response) {
+ file, fileHeader, err := request.GetFile("file")
+ if err != nil {
+ log.Println(err)
+ response.Redirect(ctx.GetRoute("import"))
+ return
+ }
+ defer file.Close()
+
+ user := ctx.GetLoggedUser()
+ log.Printf("[UI:UploadOPML] User #%d uploaded this file: %s (%d bytes)\n", user.ID, fileHeader.Filename, fileHeader.Size)
+
+ if impErr := c.opmlHandler.Import(user.ID, file); impErr != nil {
+ args, err := c.getCommonTemplateArgs(ctx)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ response.Html().Render("import", args.Merge(tplParams{
+ "errorMessage": impErr.Error(),
+ "menu": "feeds",
+ }))
+
+ return
+ }
+
+ response.Redirect(ctx.GetRoute("feeds"))
+}
diff --git a/server/ui/controller/pagination.go b/server/ui/controller/pagination.go
new file mode 100644
index 00000000..b649d900
--- /dev/null
+++ b/server/ui/controller/pagination.go
@@ -0,0 +1,46 @@
+// 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 controller
+
+const (
+ NbItemsPerPage = 100
+)
+
+type Pagination struct {
+ Route string
+ Total int
+ Offset int
+ ItemsPerPage int
+ ShowNext bool
+ ShowPrev bool
+ NextOffset int
+ PrevOffset int
+}
+
+func (c *Controller) getPagination(route string, total, offset int) Pagination {
+ nextOffset := 0
+ prevOffset := 0
+ showNext := (total - offset) > NbItemsPerPage
+ showPrev := offset > 0
+
+ if showNext {
+ nextOffset = offset + NbItemsPerPage
+ }
+
+ if showPrev {
+ prevOffset = offset - NbItemsPerPage
+ }
+
+ return Pagination{
+ Route: route,
+ Total: total,
+ Offset: offset,
+ ItemsPerPage: NbItemsPerPage,
+ ShowNext: showNext,
+ NextOffset: nextOffset,
+ ShowPrev: showPrev,
+ PrevOffset: prevOffset,
+ }
+}
diff --git a/server/ui/controller/proxy.go b/server/ui/controller/proxy.go
new file mode 100644
index 00000000..8a2f2bfa
--- /dev/null
+++ b/server/ui/controller/proxy.go
@@ -0,0 +1,49 @@
+// 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 controller
+
+import (
+ "encoding/base64"
+ "errors"
+ "github.com/miniflux/miniflux2/helper"
+ "github.com/miniflux/miniflux2/server/core"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "time"
+)
+
+func (c *Controller) ImageProxy(ctx *core.Context, request *core.Request, response *core.Response) {
+ encodedURL := request.GetStringParam("encodedURL", "")
+ if encodedURL == "" {
+ response.Html().BadRequest(errors.New("No URL provided"))
+ return
+ }
+
+ decodedURL, err := base64.StdEncoding.DecodeString(encodedURL)
+ if err != nil {
+ response.Html().BadRequest(errors.New("Unable to decode this URL"))
+ return
+ }
+
+ resp, err := http.Get(string(decodedURL))
+ if err != nil {
+ log.Println(err)
+ response.Html().NotFound()
+ return
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ response.Html().NotFound()
+ return
+ }
+
+ body, _ := ioutil.ReadAll(resp.Body)
+ etag := helper.HashFromBytes(body)
+ contentType := resp.Header.Get("Content-Type")
+
+ response.Cache(contentType, etag, body, 72*time.Hour)
+}
diff --git a/server/ui/controller/session.go b/server/ui/controller/session.go
new file mode 100644
index 00000000..0255728f
--- /dev/null
+++ b/server/ui/controller/session.go
@@ -0,0 +1,49 @@
+// 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 controller
+
+import (
+ "github.com/miniflux/miniflux2/server/core"
+ "log"
+)
+
+func (c *Controller) ShowSessions(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+ args, err := c.getCommonTemplateArgs(ctx)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ sessions, err := c.store.GetSessions(user.ID)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ sessionCookie := request.GetCookie("sessionID")
+ response.Html().Render("sessions", args.Merge(tplParams{
+ "sessions": sessions,
+ "currentSessionToken": sessionCookie,
+ "menu": "settings",
+ }))
+}
+
+func (c *Controller) RemoveSession(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ sessionID, err := request.GetIntegerParam("sessionID")
+ if err != nil {
+ response.Html().BadRequest(err)
+ return
+ }
+
+ err = c.store.RemoveSessionByID(user.ID, sessionID)
+ if err != nil {
+ log.Println("[UI:RemoveSession]", err)
+ }
+
+ response.Redirect(ctx.GetRoute("sessions"))
+}
diff --git a/server/ui/controller/settings.go b/server/ui/controller/settings.go
new file mode 100644
index 00000000..a7cca789
--- /dev/null
+++ b/server/ui/controller/settings.go
@@ -0,0 +1,92 @@
+// 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 controller
+
+import (
+ "github.com/miniflux/miniflux2/locale"
+ "github.com/miniflux/miniflux2/model"
+ "github.com/miniflux/miniflux2/server/core"
+ "github.com/miniflux/miniflux2/server/ui/form"
+ "log"
+)
+
+func (c *Controller) ShowSettings(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ args, err := c.getSettingsFormTemplateArgs(ctx, user, nil)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ response.Html().Render("settings", args)
+}
+
+func (c *Controller) UpdateSettings(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ settingsForm := form.NewSettingsForm(request.GetRequest())
+ 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", args.Merge(tplParams{
+ "form": settingsForm,
+ "errorMessage": err.Error(),
+ }))
+ return
+ }
+
+ if c.store.AnotherUserExists(user.ID, settingsForm.Username) {
+ response.Html().Render("settings", args.Merge(tplParams{
+ "form": settingsForm,
+ "errorMessage": "This user already exists.",
+ }))
+ return
+ }
+
+ err = c.store.UpdateUser(settingsForm.Merge(user))
+ if err != nil {
+ log.Println(err)
+ response.Html().Render("settings", args.Merge(tplParams{
+ "form": settingsForm,
+ "errorMessage": "Unable to update this user.",
+ }))
+ return
+ }
+
+ response.Redirect(ctx.GetRoute("settings"))
+}
+
+func (c *Controller) getSettingsFormTemplateArgs(ctx *core.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,
+ }
+ } else {
+ args["form"] = settingsForm
+ }
+
+ args["menu"] = "settings"
+ args["themes"] = model.GetThemes()
+ args["languages"] = locale.GetAvailableLanguages()
+ args["timezones"], err = c.store.GetTimezones()
+ if err != nil {
+ return args, err
+ }
+
+ return args, nil
+}
diff --git a/server/ui/controller/static.go b/server/ui/controller/static.go
new file mode 100644
index 00000000..7b6a1def
--- /dev/null
+++ b/server/ui/controller/static.go
@@ -0,0 +1,41 @@
+// 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 controller
+
+import (
+ "encoding/base64"
+ "github.com/miniflux/miniflux2/server/core"
+ "github.com/miniflux/miniflux2/server/static"
+ "log"
+ "time"
+)
+
+func (c *Controller) Stylesheet(ctx *core.Context, request *core.Request, response *core.Response) {
+ stylesheet := request.GetStringParam("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", etag, []byte(body), 48*time.Hour)
+}
+
+func (c *Controller) Javascript(ctx *core.Context, request *core.Request, response *core.Response) {
+ response.Cache("text/javascript", static.JavascriptChecksums["app"], []byte(static.Javascript["app"]), 48*time.Hour)
+}
+
+func (c *Controller) Favicon(ctx *core.Context, request *core.Request, response *core.Response) {
+ blob, err := base64.StdEncoding.DecodeString(static.Binaries["favicon.ico"])
+ if err != nil {
+ log.Println(err)
+ response.Html().NotFound()
+ return
+ }
+
+ response.Cache("image/x-icon", static.BinariesChecksums["favicon.ico"], blob, 48*time.Hour)
+}
diff --git a/server/ui/controller/subscription.go b/server/ui/controller/subscription.go
new file mode 100644
index 00000000..b1557696
--- /dev/null
+++ b/server/ui/controller/subscription.go
@@ -0,0 +1,127 @@
+// 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 controller
+
+import (
+ "github.com/miniflux/miniflux2/model"
+ "github.com/miniflux/miniflux2/reader/subscription"
+ "github.com/miniflux/miniflux2/server/core"
+ "github.com/miniflux/miniflux2/server/ui/form"
+ "log"
+)
+
+func (c *Controller) AddSubscription(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ args, err := c.getSubscriptionFormTemplateArgs(ctx, user)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ response.Html().Render("add_subscription", args)
+}
+
+func (c *Controller) SubmitSubscription(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ args, err := c.getSubscriptionFormTemplateArgs(ctx, user)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ subscriptionForm := form.NewSubscriptionForm(request.GetRequest())
+ if err := subscriptionForm.Validate(); err != nil {
+ response.Html().Render("add_subscription", args.Merge(tplParams{
+ "form": subscriptionForm,
+ "errorMessage": err.Error(),
+ }))
+ return
+ }
+
+ subscriptions, err := subscription.FindSubscriptions(subscriptionForm.URL)
+ if err != nil {
+ log.Println(err)
+ response.Html().Render("add_subscription", args.Merge(tplParams{
+ "form": subscriptionForm,
+ "errorMessage": err,
+ }))
+ return
+ }
+
+ log.Println("[UI:SubmitSubscription]", subscriptions)
+
+ n := len(subscriptions)
+ switch {
+ case n == 0:
+ response.Html().Render("add_subscription", 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)
+ if err != nil {
+ response.Html().Render("add_subscription", args.Merge(tplParams{
+ "form": subscriptionForm,
+ "errorMessage": err,
+ }))
+ return
+ }
+
+ response.Redirect(ctx.GetRoute("feedEntries", "feedID", feed.ID))
+ case n > 1:
+ response.Html().Render("choose_subscription", args.Merge(tplParams{
+ "categoryID": subscriptionForm.CategoryID,
+ "subscriptions": subscriptions,
+ }))
+ }
+}
+
+func (c *Controller) ChooseSubscription(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ args, err := c.getSubscriptionFormTemplateArgs(ctx, user)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ subscriptionForm := form.NewSubscriptionForm(request.GetRequest())
+ if err := subscriptionForm.Validate(); err != nil {
+ response.Html().Render("add_subscription", args.Merge(tplParams{
+ "form": subscriptionForm,
+ "errorMessage": err.Error(),
+ }))
+ return
+ }
+
+ feed, err := c.feedHandler.CreateFeed(user.ID, subscriptionForm.CategoryID, subscriptionForm.URL)
+ if err != nil {
+ response.Html().Render("add_subscription", args.Merge(tplParams{
+ "form": subscriptionForm,
+ "errorMessage": err,
+ }))
+ return
+ }
+
+ response.Redirect(ctx.GetRoute("feedEntries", "feedID", feed.ID))
+}
+
+func (c *Controller) getSubscriptionFormTemplateArgs(ctx *core.Context, user *model.User) (tplParams, error) {
+ args, err := c.getCommonTemplateArgs(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ categories, err := c.store.GetCategories(user.ID)
+ if err != nil {
+ return nil, err
+ }
+
+ args["categories"] = categories
+ args["menu"] = "feeds"
+ return args, nil
+}
diff --git a/server/ui/controller/unread.go b/server/ui/controller/unread.go
new file mode 100644
index 00000000..63d7db02
--- /dev/null
+++ b/server/ui/controller/unread.go
@@ -0,0 +1,43 @@
+// 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 controller
+
+import (
+ "github.com/miniflux/miniflux2/model"
+ "github.com/miniflux/miniflux2/server/core"
+)
+
+func (c *Controller) ShowUnreadPage(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+ offset := request.GetQueryIntegerParam("offset", 0)
+
+ builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithStatus(model.EntryStatusUnread)
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(model.DefaultSortingDirection)
+ builder.WithOffset(offset)
+ builder.WithLimit(NbItemsPerPage)
+
+ entries, err := builder.GetEntries()
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ countUnread, err := builder.CountEntries()
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ response.Html().Render("unread", tplParams{
+ "user": user,
+ "countUnread": countUnread,
+ "entries": entries,
+ "pagination": c.getPagination(ctx.GetRoute("unread"), countUnread, offset),
+ "menu": "unread",
+ "csrf": ctx.GetCsrfToken(),
+ })
+}
diff --git a/server/ui/controller/user.go b/server/ui/controller/user.go
new file mode 100644
index 00000000..c69b0f8d
--- /dev/null
+++ b/server/ui/controller/user.go
@@ -0,0 +1,231 @@
+// 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 controller
+
+import (
+ "errors"
+ "github.com/miniflux/miniflux2/model"
+ "github.com/miniflux/miniflux2/server/core"
+ "github.com/miniflux/miniflux2/server/ui/form"
+ "log"
+)
+
+func (c *Controller) ShowUsers(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ if !user.IsAdmin {
+ response.Html().Forbidden()
+ return
+ }
+
+ args, err := c.getCommonTemplateArgs(ctx)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ users, err := c.store.GetUsers()
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ response.Html().Render("users", args.Merge(tplParams{
+ "users": users,
+ "menu": "settings",
+ }))
+}
+
+func (c *Controller) CreateUser(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ 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", args.Merge(tplParams{
+ "menu": "settings",
+ "form": &form.UserForm{},
+ }))
+}
+
+func (c *Controller) SaveUser(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ if !user.IsAdmin {
+ response.Html().Forbidden()
+ return
+ }
+
+ args, err := c.getCommonTemplateArgs(ctx)
+ if err != nil {
+ response.Html().ServerError(err)
+ return
+ }
+
+ userForm := form.NewUserForm(request.GetRequest())
+ if err := userForm.ValidateCreation(); err != nil {
+ response.Html().Render("create_user", args.Merge(tplParams{
+ "menu": "settings",
+ "form": userForm,
+ "errorMessage": err.Error(),
+ }))
+ return
+ }
+
+ if c.store.UserExists(userForm.Username) {
+ response.Html().Render("create_user", args.Merge(tplParams{
+ "menu": "settings",
+ "form": userForm,
+ "errorMessage": "This user already exists.",
+ }))
+ return
+ }
+
+ newUser := userForm.ToUser()
+ if err := c.store.CreateUser(newUser); err != nil {
+ log.Println(err)
+ response.Html().Render("edit_user", args.Merge(tplParams{
+ "menu": "settings",
+ "form": userForm,
+ "errorMessage": "Unable to create this user.",
+ }))
+ return
+ }
+
+ response.Redirect(ctx.GetRoute("users"))
+}
+
+func (c *Controller) EditUser(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ 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", args.Merge(tplParams{
+ "menu": "settings",
+ "selected_user": selectedUser,
+ "form": &form.UserForm{
+ Username: selectedUser.Username,
+ IsAdmin: selectedUser.IsAdmin,
+ },
+ }))
+}
+
+func (c *Controller) UpdateUser(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+
+ 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.GetRequest())
+ if err := userForm.ValidateModification(); err != nil {
+ response.Html().Render("edit_user", 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", 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 {
+ log.Println(err)
+ response.Html().Render("edit_user", args.Merge(tplParams{
+ "menu": "settings",
+ "selected_user": selectedUser,
+ "form": userForm,
+ "errorMessage": "Unable to update this user.",
+ }))
+ return
+ }
+
+ response.Redirect(ctx.GetRoute("users"))
+}
+
+func (c *Controller) RemoveUser(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.GetLoggedUser()
+ 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.GetRoute("users"))
+}
+
+func (c *Controller) getUserFromURL(ctx *core.Context, request *core.Request, response *core.Response) (*model.User, error) {
+ userID, err := request.GetIntegerParam("userID")
+ if err != nil {
+ response.Html().BadRequest(err)
+ return nil, err
+ }
+
+ user, err := c.store.GetUserById(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
+}