Use vanilla HTTP handlers (refactoring)
This commit is contained in:
parent
1eba1730d1
commit
f49b42f70f
121 changed files with 4339 additions and 3369 deletions
31
ui/about.go
31
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"))
|
||||
}
|
||||
|
|
61
ui/bookmark_entries.go
Normal file
61
ui/bookmark_entries.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// ShowStarredPage renders the page with all starred entries.
|
||||
func (c *Controller) ShowStarredPage(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
offset := request.QueryIntParam(r, "offset", 0)
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithStarred()
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(nbItemsPerPage)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
|
||||
view.Set("total", count)
|
||||
view.Set("entries", entries)
|
||||
view.Set("pagination", c.getPagination(route.Path(c.router, "starred"), count, offset))
|
||||
view.Set("menu", "starred")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("starred"))
|
||||
}
|
257
ui/category.go
257
ui/category.go
|
@ -1,257 +0,0 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
)
|
||||
|
||||
// ShowCategories shows the page with all categories.
|
||||
func (c *Controller) ShowCategories(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.LoggedUser()
|
||||
categories, err := c.store.CategoriesWithFeedCount(user.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("categories", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"categories": categories,
|
||||
"total": len(categories),
|
||||
"menu": "categories",
|
||||
}))
|
||||
}
|
||||
|
||||
// ShowCategoryEntries shows all entries for the given category.
|
||||
func (c *Controller) ShowCategoryEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
offset := request.QueryIntegerParam("offset", 0)
|
||||
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := c.getCategoryFromURL(ctx, request, response)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithCategoryID(category.ID)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(nbItemsPerPage)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("category_entries", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"category": category,
|
||||
"entries": entries,
|
||||
"total": count,
|
||||
"pagination": c.getPagination(ctx.Route("categoryEntries", "categoryID", category.ID), count, offset),
|
||||
"menu": "categories",
|
||||
}))
|
||||
}
|
||||
|
||||
// CreateCategory shows the form to create a new category.
|
||||
func (c *Controller) CreateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("create_category", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"menu": "categories",
|
||||
}))
|
||||
}
|
||||
|
||||
// SaveCategory validate and save the new category into the database.
|
||||
func (c *Controller) SaveCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
categoryForm := form.NewCategoryForm(request.Request())
|
||||
if err := categoryForm.Validate(); err != nil {
|
||||
response.HTML().Render("create_category", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"errorMessage": err.Error(),
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
duplicateCategory, err := c.store.CategoryByTitle(user.ID, categoryForm.Title)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if duplicateCategory != nil {
|
||||
response.HTML().Render("create_category", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"errorMessage": "This category already exists.",
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
category := model.Category{Title: categoryForm.Title, UserID: user.ID}
|
||||
err = c.store.CreateCategory(&category)
|
||||
if err != nil {
|
||||
logger.Info("[Controller:CreateCategory] %v", err)
|
||||
response.HTML().Render("create_category", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"errorMessage": "Unable to create this category.",
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(ctx.Route("categories"))
|
||||
}
|
||||
|
||||
// EditCategory shows the form to modify a category.
|
||||
func (c *Controller) EditCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
category, err := c.getCategoryFromURL(ctx, request, response)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:EditCategory] %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
args, err := c.getCategoryFormTemplateArgs(ctx, user, category, nil)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("edit_category", ctx.UserLanguage(), args)
|
||||
}
|
||||
|
||||
// UpdateCategory validate and update a category.
|
||||
func (c *Controller) UpdateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
category, err := c.getCategoryFromURL(ctx, request, response)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:UpdateCategory] %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
categoryForm := form.NewCategoryForm(request.Request())
|
||||
args, err := c.getCategoryFormTemplateArgs(ctx, user, category, categoryForm)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := categoryForm.Validate(); err != nil {
|
||||
response.HTML().Render("edit_category", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"errorMessage": err.Error(),
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
if c.store.AnotherCategoryExists(user.ID, category.ID, categoryForm.Title) {
|
||||
response.HTML().Render("edit_category", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"errorMessage": "This category already exists.",
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.UpdateCategory(categoryForm.Merge(category))
|
||||
if err != nil {
|
||||
logger.Error("[Controller:UpdateCategory] %v", err)
|
||||
response.HTML().Render("edit_category", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"errorMessage": "Unable to update this category.",
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(ctx.Route("categories"))
|
||||
}
|
||||
|
||||
// RemoveCategory delete a category from the database.
|
||||
func (c *Controller) RemoveCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
category, err := c.getCategoryFromURL(ctx, request, response)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.RemoveCategory(user.ID, category.ID); err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(ctx.Route("categories"))
|
||||
}
|
||||
|
||||
func (c *Controller) getCategoryFromURL(ctx *handler.Context, request *handler.Request, response *handler.Response) (*model.Category, error) {
|
||||
categoryID, err := request.IntegerParam("categoryID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := ctx.LoggedUser()
|
||||
category, err := c.store.Category(user.ID, categoryID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if category == nil {
|
||||
response.HTML().NotFound()
|
||||
return nil, errors.New("Category not found")
|
||||
}
|
||||
|
||||
return category, nil
|
||||
}
|
||||
|
||||
func (c *Controller) getCategoryFormTemplateArgs(ctx *handler.Context, user *model.User, category *model.Category, categoryForm *form.CategoryForm) (tplParams, error) {
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if categoryForm == nil {
|
||||
args["form"] = form.CategoryForm{
|
||||
Title: category.Title,
|
||||
}
|
||||
} else {
|
||||
args["form"] = categoryForm
|
||||
}
|
||||
|
||||
args["category"] = category
|
||||
args["menu"] = "categories"
|
||||
return args, nil
|
||||
}
|
33
ui/category_create.go
Normal file
33
ui/category_create.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// CreateCategory shows the form to create a new category.
|
||||
func (c *Controller) CreateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("menu", "categories")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("create_category"))
|
||||
}
|
58
ui/category_edit.go
Normal file
58
ui/category_edit.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// EditCategory shows the form to modify a category.
|
||||
func (c *Controller) EditCategory(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
categoryID, err := request.IntParam(r, "categoryID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := c.store.Category(ctx.UserID(), categoryID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if category == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
categoryForm := form.CategoryForm{
|
||||
Title: category.Title,
|
||||
}
|
||||
|
||||
view.Set("form", categoryForm)
|
||||
view.Set("category", category)
|
||||
view.Set("menu", "categories")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("edit_category"))
|
||||
}
|
78
ui/category_entries.go
Normal file
78
ui/category_entries.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// CategoryEntries shows all entries for the given category.
|
||||
func (c *Controller) CategoryEntries(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
categoryID, err := request.IntParam(r, "categoryID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := c.store.Category(ctx.UserID(), categoryID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if category == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
offset := request.QueryIntParam(r, "offset", 0)
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithCategoryID(category.ID)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(nbItemsPerPage)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("category", category)
|
||||
view.Set("total", count)
|
||||
view.Set("entries", entries)
|
||||
view.Set("pagination", c.getPagination(route.Path(c.router, "categoryEntries", "categoryID", category.ID), count, offset))
|
||||
view.Set("menu", "categories")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("category_entries"))
|
||||
}
|
41
ui/category_list.go
Normal file
41
ui/category_list.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// CategoryList shows the page with all categories.
|
||||
func (c *Controller) CategoryList(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
categories, err := c.store.CategoriesWithFeedCount(user.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("categories", categories)
|
||||
view.Set("total", len(categories))
|
||||
view.Set("menu", "categories")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("categories"))
|
||||
}
|
50
ui/category_remove.go
Normal file
50
ui/category_remove.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
)
|
||||
|
||||
// RemoveCategory deletes a category from the database.
|
||||
func (c *Controller) RemoveCategory(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
categoryID, err := request.IntParam(r, "categoryID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := c.store.Category(ctx.UserID(), categoryID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if category == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.RemoveCategory(user.ID, category.ID); err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(w, r, route.Path(c.router, "categories"))
|
||||
}
|
71
ui/category_save.go
Normal file
71
ui/category_save.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// SaveCategory validate and save the new category into the database.
|
||||
func (c *Controller) SaveCategory(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
categoryForm := form.NewCategoryForm(r)
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("form", categoryForm)
|
||||
view.Set("menu", "categories")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
if err := categoryForm.Validate(); err != nil {
|
||||
view.Set("errorMessage", err.Error())
|
||||
html.OK(w, view.Render("create_category"))
|
||||
return
|
||||
}
|
||||
|
||||
duplicateCategory, err := c.store.CategoryByTitle(user.ID, categoryForm.Title)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if duplicateCategory != nil {
|
||||
view.Set("errorMessage", "This category already exists.")
|
||||
html.OK(w, view.Render("create_category"))
|
||||
return
|
||||
}
|
||||
|
||||
category := model.Category{
|
||||
Title: categoryForm.Title,
|
||||
UserID: user.ID,
|
||||
}
|
||||
|
||||
if err = c.store.CreateCategory(&category); err != nil {
|
||||
logger.Error("[Controller:CreateCategory] %v", err)
|
||||
view.Set("errorMessage", "Unable to create this category.")
|
||||
html.OK(w, view.Render("create_category"))
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(w, r, route.Path(c.router, "categories"))
|
||||
}
|
79
ui/category_update.go
Normal file
79
ui/category_update.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// UpdateCategory validates and updates a category.
|
||||
func (c *Controller) UpdateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
categoryID, err := request.IntParam(r, "categoryID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := c.store.Category(ctx.UserID(), categoryID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if category == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
categoryForm := form.NewCategoryForm(r)
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("form", categoryForm)
|
||||
view.Set("category", category)
|
||||
view.Set("menu", "categories")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
if err := categoryForm.Validate(); err != nil {
|
||||
view.Set("errorMessage", err.Error())
|
||||
html.OK(w, view.Render("edit_category"))
|
||||
return
|
||||
}
|
||||
|
||||
if c.store.AnotherCategoryExists(user.ID, category.ID, categoryForm.Title) {
|
||||
view.Set("errorMessage", "This category already exists.")
|
||||
html.OK(w, view.Render("edit_category"))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.UpdateCategory(categoryForm.Merge(category))
|
||||
if err != nil {
|
||||
logger.Error("[Controller:UpdateCategory] %v", err)
|
||||
view.Set("errorMessage", "Unable to update this category.")
|
||||
html.OK(w, view.Render("edit_category"))
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(w, r, route.Path(c.router, "categories"))
|
||||
}
|
|
@ -5,59 +5,35 @@
|
|||
package ui
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
494
ui/entry.go
494
ui/entry.go
|
@ -1,494 +0,0 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/integration"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/reader/sanitizer"
|
||||
"github.com/miniflux/miniflux/reader/scraper"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
)
|
||||
|
||||
// FetchContent downloads the original HTML page and returns relevant contents.
|
||||
func (c *Controller) FetchContent(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.LoggedUser()
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
response.JSON().NotFound(errors.New("Entry not found"))
|
||||
return
|
||||
}
|
||||
|
||||
content, err := scraper.Fetch(entry.URL, entry.Feed.ScraperRules)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
entry.Content = sanitizer.Sanitize(entry.URL, content)
|
||||
c.store.UpdateEntryContent(entry)
|
||||
|
||||
response.JSON().Created(map[string]string{"content": entry.Content})
|
||||
}
|
||||
|
||||
// SaveEntry send the link to external services.
|
||||
func (c *Controller) SaveEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.LoggedUser()
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
response.JSON().NotFound(errors.New("Entry not found"))
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := c.store.Integration(user.ID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
integration.SendEntry(entry, settings)
|
||||
}()
|
||||
|
||||
response.JSON().Created(map[string]string{"message": "saved"})
|
||||
}
|
||||
|
||||
// ShowFeedEntry shows a single feed entry in "feed" mode.
|
||||
func (c *Controller) ShowFeedEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithFeedID(feedID)
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
response.HTML().NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if entry.Status == model.EntryStatusUnread {
|
||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:ShowFeedEntry] %v", err)
|
||||
response.HTML().ServerError(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder = c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithFeedID(feedID)
|
||||
|
||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
nextEntryRoute := ""
|
||||
if nextEntry != nil {
|
||||
nextEntryRoute = ctx.Route("feedEntry", "feedID", feedID, "entryID", nextEntry.ID)
|
||||
}
|
||||
|
||||
prevEntryRoute := ""
|
||||
if prevEntry != nil {
|
||||
prevEntryRoute = ctx.Route("feedEntry", "feedID", feedID, "entryID", prevEntry.ID)
|
||||
}
|
||||
|
||||
response.HTML().Render("entry", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"entry": entry,
|
||||
"prevEntry": prevEntry,
|
||||
"nextEntry": nextEntry,
|
||||
"nextEntryRoute": nextEntryRoute,
|
||||
"prevEntryRoute": prevEntryRoute,
|
||||
"menu": "feeds",
|
||||
}))
|
||||
}
|
||||
|
||||
// ShowCategoryEntry shows a single feed entry in "category" mode.
|
||||
func (c *Controller) ShowCategoryEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
categoryID, err := request.IntegerParam("categoryID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithCategoryID(categoryID)
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
response.HTML().NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if entry.Status == model.EntryStatusUnread {
|
||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:ShowCategoryEntry] %v", err)
|
||||
response.HTML().ServerError(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder = c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithCategoryID(categoryID)
|
||||
|
||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
nextEntryRoute := ""
|
||||
if nextEntry != nil {
|
||||
nextEntryRoute = ctx.Route("categoryEntry", "categoryID", categoryID, "entryID", nextEntry.ID)
|
||||
}
|
||||
|
||||
prevEntryRoute := ""
|
||||
if prevEntry != nil {
|
||||
prevEntryRoute = ctx.Route("categoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID)
|
||||
}
|
||||
|
||||
response.HTML().Render("entry", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"entry": entry,
|
||||
"prevEntry": prevEntry,
|
||||
"nextEntry": nextEntry,
|
||||
"nextEntryRoute": nextEntryRoute,
|
||||
"prevEntryRoute": prevEntryRoute,
|
||||
"menu": "categories",
|
||||
}))
|
||||
}
|
||||
|
||||
// ShowUnreadEntry shows a single feed entry in "unread" mode.
|
||||
func (c *Controller) ShowUnreadEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
response.HTML().NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
builder = c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithStatus(model.EntryStatusUnread)
|
||||
|
||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
nextEntryRoute := ""
|
||||
if nextEntry != nil {
|
||||
nextEntryRoute = ctx.Route("unreadEntry", "entryID", nextEntry.ID)
|
||||
}
|
||||
|
||||
prevEntryRoute := ""
|
||||
if prevEntry != nil {
|
||||
prevEntryRoute = ctx.Route("unreadEntry", "entryID", prevEntry.ID)
|
||||
}
|
||||
|
||||
// We change the status here, otherwise we cannot get the pagination for unread items.
|
||||
if entry.Status == model.EntryStatusUnread {
|
||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:ShowUnreadEntry] %v", err)
|
||||
response.HTML().ServerError(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// The unread counter have to be fetched after changing the entry status
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("entry", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"entry": entry,
|
||||
"prevEntry": prevEntry,
|
||||
"nextEntry": nextEntry,
|
||||
"nextEntryRoute": nextEntryRoute,
|
||||
"prevEntryRoute": prevEntryRoute,
|
||||
"menu": "unread",
|
||||
}))
|
||||
}
|
||||
|
||||
// ShowReadEntry shows a single feed entry in "history" mode.
|
||||
func (c *Controller) ShowReadEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
response.HTML().NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder = c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithStatus(model.EntryStatusRead)
|
||||
|
||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
nextEntryRoute := ""
|
||||
if nextEntry != nil {
|
||||
nextEntryRoute = ctx.Route("readEntry", "entryID", nextEntry.ID)
|
||||
}
|
||||
|
||||
prevEntryRoute := ""
|
||||
if prevEntry != nil {
|
||||
prevEntryRoute = ctx.Route("readEntry", "entryID", prevEntry.ID)
|
||||
}
|
||||
|
||||
response.HTML().Render("entry", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"entry": entry,
|
||||
"prevEntry": prevEntry,
|
||||
"nextEntry": nextEntry,
|
||||
"nextEntryRoute": nextEntryRoute,
|
||||
"prevEntryRoute": prevEntryRoute,
|
||||
"menu": "history",
|
||||
}))
|
||||
}
|
||||
|
||||
// ShowStarredEntry shows a single feed entry in "starred" mode.
|
||||
func (c *Controller) ShowStarredEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
response.HTML().NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if entry.Status == model.EntryStatusUnread {
|
||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:ShowReadEntry] %v", err)
|
||||
response.HTML().ServerError(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder = c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithStarred()
|
||||
|
||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
nextEntryRoute := ""
|
||||
if nextEntry != nil {
|
||||
nextEntryRoute = ctx.Route("starredEntry", "entryID", nextEntry.ID)
|
||||
}
|
||||
|
||||
prevEntryRoute := ""
|
||||
if prevEntry != nil {
|
||||
prevEntryRoute = ctx.Route("starredEntry", "entryID", prevEntry.ID)
|
||||
}
|
||||
|
||||
response.HTML().Render("entry", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"entry": entry,
|
||||
"prevEntry": prevEntry,
|
||||
"nextEntry": nextEntry,
|
||||
"nextEntryRoute": nextEntryRoute,
|
||||
"prevEntryRoute": prevEntryRoute,
|
||||
"menu": "starred",
|
||||
}))
|
||||
}
|
||||
|
||||
// UpdateEntriesStatus handles Ajax request to update the status for a list of entries.
|
||||
func (c *Controller) UpdateEntriesStatus(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
entryIDs, status, err := decodeEntryStatusPayload(request.Body())
|
||||
if err != nil {
|
||||
logger.Error("[Controller:UpdateEntryStatus] %v", err)
|
||||
response.JSON().BadRequest(nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(entryIDs) == 0 {
|
||||
response.JSON().BadRequest(errors.New("The list of entryID is empty"))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.SetEntriesStatus(user.ID, entryIDs, status)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:UpdateEntryStatus] %v", err)
|
||||
response.JSON().ServerError(nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard("OK")
|
||||
}
|
||||
|
||||
func (c *Controller) getEntryPrevNext(user *model.User, builder *storage.EntryQueryBuilder, entryID int64) (prev *model.Entry, next *model.Entry, err error) {
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
n := len(entries)
|
||||
for i := 0; i < n; i++ {
|
||||
if entries[i].ID == entryID {
|
||||
if i-1 >= 0 {
|
||||
prev = entries[i-1]
|
||||
}
|
||||
|
||||
if i+1 < n {
|
||||
next = entries[i+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return prev, next, nil
|
||||
}
|
98
ui/entry_category.go
Normal file
98
ui/entry_category.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// ShowCategoryEntry shows a single feed entry in "category" mode.
|
||||
func (c *Controller) ShowCategoryEntry(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
categoryID, err := request.IntParam(r, "categoryID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
entryID, err := request.IntParam(r, "entryID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithCategoryID(categoryID)
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
if entry.Status == model.EntryStatusUnread {
|
||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:ShowCategoryEntry] %v", err)
|
||||
html.ServerError(w, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
builder = c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithCategoryID(categoryID)
|
||||
|
||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
nextEntryRoute := ""
|
||||
if nextEntry != nil {
|
||||
nextEntryRoute = route.Path(c.router, "categoryEntry", "categoryID", categoryID, "entryID", nextEntry.ID)
|
||||
}
|
||||
|
||||
prevEntryRoute := ""
|
||||
if prevEntry != nil {
|
||||
prevEntryRoute = route.Path(c.router, "categoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID)
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("entry", entry)
|
||||
view.Set("prevEntry", prevEntry)
|
||||
view.Set("nextEntry", nextEntry)
|
||||
view.Set("nextEntryRoute", nextEntryRoute)
|
||||
view.Set("prevEntryRoute", prevEntryRoute)
|
||||
view.Set("menu", "categories")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("entry"))
|
||||
}
|
98
ui/entry_feed.go
Normal file
98
ui/entry_feed.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// ShowFeedEntry shows a single feed entry in "feed" mode.
|
||||
func (c *Controller) ShowFeedEntry(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
entryID, err := request.IntParam(r, "entryID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
feedID, err := request.IntParam(r, "feedID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithFeedID(feedID)
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
if entry.Status == model.EntryStatusUnread {
|
||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:ShowFeedEntry] %v", err)
|
||||
html.ServerError(w, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
builder = c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithFeedID(feedID)
|
||||
|
||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
nextEntryRoute := ""
|
||||
if nextEntry != nil {
|
||||
nextEntryRoute = route.Path(c.router, "feedEntry", "feedID", feedID, "entryID", nextEntry.ID)
|
||||
}
|
||||
|
||||
prevEntryRoute := ""
|
||||
if prevEntry != nil {
|
||||
prevEntryRoute = route.Path(c.router, "feedEntry", "feedID", feedID, "entryID", prevEntry.ID)
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("entry", entry)
|
||||
view.Set("prevEntry", prevEntry)
|
||||
view.Set("nextEntry", nextEntry)
|
||||
view.Set("nextEntryRoute", nextEntryRoute)
|
||||
view.Set("prevEntryRoute", prevEntryRoute)
|
||||
view.Set("menu", "feeds")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("entry"))
|
||||
}
|
36
ui/entry_prev_next.go
Normal file
36
ui/entry_prev_next.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
)
|
||||
|
||||
func (c *Controller) getEntryPrevNext(user *model.User, builder *storage.EntryQueryBuilder, entryID int64) (prev *model.Entry, next *model.Entry, err error) {
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
n := len(entries)
|
||||
for i := 0; i < n; i++ {
|
||||
if entries[i].ID == entryID {
|
||||
if i-1 >= 0 {
|
||||
prev = entries[i-1]
|
||||
}
|
||||
|
||||
if i+1 < n {
|
||||
next = entries[i+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return prev, next, nil
|
||||
}
|
81
ui/entry_read.go
Normal file
81
ui/entry_read.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// ShowReadEntry shows a single feed entry in "history" mode.
|
||||
func (c *Controller) ShowReadEntry(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
entryID, err := request.IntParam(r, "entryID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
builder = c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithStatus(model.EntryStatusRead)
|
||||
|
||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
nextEntryRoute := ""
|
||||
if nextEntry != nil {
|
||||
nextEntryRoute = route.Path(c.router, "readEntry", "entryID", nextEntry.ID)
|
||||
}
|
||||
|
||||
prevEntryRoute := ""
|
||||
if prevEntry != nil {
|
||||
prevEntryRoute = route.Path(c.router, "readEntry", "entryID", prevEntry.ID)
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("entry", entry)
|
||||
view.Set("prevEntry", prevEntry)
|
||||
view.Set("nextEntry", nextEntry)
|
||||
view.Set("nextEntryRoute", nextEntryRoute)
|
||||
view.Set("prevEntryRoute", prevEntryRoute)
|
||||
view.Set("menu", "history")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("entry"))
|
||||
}
|
54
ui/entry_save.go
Normal file
54
ui/entry_save.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/json"
|
||||
"github.com/miniflux/miniflux/integration"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
)
|
||||
|
||||
// SaveEntry send the link to external services.
|
||||
func (c *Controller) SaveEntry(w http.ResponseWriter, r *http.Request) {
|
||||
entryID, err := request.IntParam(r, "entryID")
|
||||
if err != nil {
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.New(r)
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(ctx.UserID())
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
json.NotFound(w, errors.New("Entry not found"))
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := c.store.Integration(ctx.UserID())
|
||||
if err != nil {
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
integration.SendEntry(entry, settings)
|
||||
}()
|
||||
|
||||
json.Created(w, map[string]string{"message": "saved"})
|
||||
}
|
53
ui/entry_scraper.go
Normal file
53
ui/entry_scraper.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/json"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/reader/sanitizer"
|
||||
"github.com/miniflux/miniflux/reader/scraper"
|
||||
)
|
||||
|
||||
// FetchContent downloads the original HTML page and returns relevant contents.
|
||||
func (c *Controller) FetchContent(w http.ResponseWriter, r *http.Request) {
|
||||
entryID, err := request.IntParam(r, "entryID")
|
||||
if err != nil {
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.New(r)
|
||||
builder := c.store.NewEntryQueryBuilder(ctx.UserID())
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
json.NotFound(w, errors.New("Entry not found"))
|
||||
return
|
||||
}
|
||||
|
||||
content, err := scraper.Fetch(entry.URL, entry.Feed.ScraperRules)
|
||||
if err != nil {
|
||||
json.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
entry.Content = sanitizer.Sanitize(entry.URL, content)
|
||||
c.store.UpdateEntryContent(entry)
|
||||
|
||||
json.Created(w, map[string]string{"content": entry.Content})
|
||||
}
|
91
ui/entry_starred.go
Normal file
91
ui/entry_starred.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// ShowStarredEntry shows a single feed entry in "starred" mode.
|
||||
func (c *Controller) ShowStarredEntry(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
entryID, err := request.IntParam(r, "entryID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
if entry.Status == model.EntryStatusUnread {
|
||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:ShowReadEntry] %v", err)
|
||||
html.ServerError(w, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
builder = c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithStarred()
|
||||
|
||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
nextEntryRoute := ""
|
||||
if nextEntry != nil {
|
||||
nextEntryRoute = route.Path(c.router, "starredEntry", "entryID", nextEntry.ID)
|
||||
}
|
||||
|
||||
prevEntryRoute := ""
|
||||
if prevEntry != nil {
|
||||
prevEntryRoute = route.Path(c.router, "starredEntry", "entryID", prevEntry.ID)
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("entry", entry)
|
||||
view.Set("prevEntry", prevEntry)
|
||||
view.Set("nextEntry", nextEntry)
|
||||
view.Set("nextEntryRoute", nextEntryRoute)
|
||||
view.Set("prevEntryRoute", prevEntryRoute)
|
||||
view.Set("menu", "starred")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("entry"))
|
||||
}
|
32
ui/entry_toggle_bookmark.go
Normal file
32
ui/entry_toggle_bookmark.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/json"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
)
|
||||
|
||||
// ToggleBookmark handles Ajax request to toggle bookmark value.
|
||||
func (c *Controller) ToggleBookmark(w http.ResponseWriter, r *http.Request) {
|
||||
entryID, err := request.IntParam(r, "entryID")
|
||||
if err != nil {
|
||||
json.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.New(r)
|
||||
if err := c.store.ToggleBookmark(ctx.UserID(), entryID); err != nil {
|
||||
logger.Error("[Controller:ToggleBookmark] %v", err)
|
||||
json.ServerError(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
json.OK(w, "OK")
|
||||
}
|
92
ui/entry_unread.go
Normal file
92
ui/entry_unread.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// ShowUnreadEntry shows a single feed entry in "unread" mode.
|
||||
func (c *Controller) ShowUnreadEntry(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
entryID, err := request.IntParam(r, "entryID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
builder = c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithStatus(model.EntryStatusUnread)
|
||||
|
||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
nextEntryRoute := ""
|
||||
if nextEntry != nil {
|
||||
nextEntryRoute = route.Path(c.router, "unreadEntry", "entryID", nextEntry.ID)
|
||||
}
|
||||
|
||||
prevEntryRoute := ""
|
||||
if prevEntry != nil {
|
||||
prevEntryRoute = route.Path(c.router, "unreadEntry", "entryID", prevEntry.ID)
|
||||
}
|
||||
|
||||
// We change the status here, otherwise we cannot get the pagination for unread items.
|
||||
if entry.Status == model.EntryStatusUnread {
|
||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:ShowUnreadEntry] %v", err)
|
||||
html.ServerError(w, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("entry", entry)
|
||||
view.Set("prevEntry", prevEntry)
|
||||
view.Set("nextEntry", nextEntry)
|
||||
view.Set("nextEntryRoute", nextEntryRoute)
|
||||
view.Set("prevEntryRoute", prevEntryRoute)
|
||||
view.Set("menu", "unread")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("entry"))
|
||||
}
|
39
ui/entry_update_status.go
Normal file
39
ui/entry_update_status.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response/json"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
)
|
||||
|
||||
// UpdateEntriesStatus updates the status for a list of entries.
|
||||
func (c *Controller) UpdateEntriesStatus(w http.ResponseWriter, r *http.Request) {
|
||||
entryIDs, status, err := decodeEntryStatusPayload(r.Body)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:UpdateEntryStatus] %v", err)
|
||||
json.BadRequest(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(entryIDs) == 0 {
|
||||
json.BadRequest(w, errors.New("The list of entryID is empty"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.New(r)
|
||||
err = c.store.SetEntriesStatus(ctx.UserID(), entryIDs, status)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:UpdateEntryStatus] %v", err)
|
||||
json.ServerError(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
json.OK(w, "OK")
|
||||
}
|
236
ui/feed.go
236
ui/feed.go
|
@ -1,236 +0,0 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
)
|
||||
|
||||
// RefreshAllFeeds refresh all feeds in the background for the current user.
|
||||
func (c *Controller) RefreshAllFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
jobs, err := c.store.NewUserBatch(user.ID, c.store.CountFeeds(user.ID))
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
c.pool.Push(jobs)
|
||||
}()
|
||||
|
||||
response.Redirect(ctx.Route("feeds"))
|
||||
}
|
||||
|
||||
// ShowFeedsPage shows the page with all subscriptions.
|
||||
func (c *Controller) ShowFeedsPage(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
feeds, err := c.store.Feeds(user.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("feeds", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"feeds": feeds,
|
||||
"total": len(feeds),
|
||||
"menu": "feeds",
|
||||
}))
|
||||
}
|
||||
|
||||
// ShowFeedEntries shows all entries for the given feed.
|
||||
func (c *Controller) ShowFeedEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
offset := request.QueryIntegerParam("offset", 0)
|
||||
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
feed, err := c.getFeedFromURL(request, response, user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithFeedID(feed.ID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(nbItemsPerPage)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("feed_entries", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"feed": feed,
|
||||
"entries": entries,
|
||||
"total": count,
|
||||
"pagination": c.getPagination(ctx.Route("feedEntries", "feedID", feed.ID), count, offset),
|
||||
"menu": "feeds",
|
||||
}))
|
||||
}
|
||||
|
||||
// EditFeed shows the form to modify a subscription.
|
||||
func (c *Controller) EditFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
feed, err := c.getFeedFromURL(request, response, user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
args, err := c.getFeedFormTemplateArgs(ctx, user, feed, nil)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("edit_feed", ctx.UserLanguage(), args)
|
||||
}
|
||||
|
||||
// UpdateFeed update a subscription and redirect to the feed entries page.
|
||||
func (c *Controller) UpdateFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
feed, err := c.getFeedFromURL(request, response, user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
feedForm := form.NewFeedForm(request.Request())
|
||||
args, err := c.getFeedFormTemplateArgs(ctx, user, feed, feedForm)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := feedForm.ValidateModification(); err != nil {
|
||||
response.HTML().Render("edit_feed", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"errorMessage": err.Error(),
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.UpdateFeed(feedForm.Merge(feed))
|
||||
if err != nil {
|
||||
logger.Error("[Controller:EditFeed] %v", err)
|
||||
response.HTML().Render("edit_feed", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"errorMessage": "Unable to update this feed.",
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(ctx.Route("feedEntries", "feedID", feed.ID))
|
||||
}
|
||||
|
||||
// RemoveFeed delete a subscription from the database and redirect to the list of feeds page.
|
||||
func (c *Controller) RemoveFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.LoggedUser()
|
||||
if err := c.store.RemoveFeed(user.ID, feedID); err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(ctx.Route("feeds"))
|
||||
}
|
||||
|
||||
// RefreshFeed refresh a subscription and redirect to the feed entries page.
|
||||
func (c *Controller) RefreshFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.LoggedUser()
|
||||
if err := c.feedHandler.RefreshFeed(user.ID, feedID); err != nil {
|
||||
logger.Error("[Controller:RefreshFeed] %v", err)
|
||||
}
|
||||
|
||||
response.Redirect(ctx.Route("feedEntries", "feedID", feedID))
|
||||
}
|
||||
|
||||
func (c *Controller) getFeedFromURL(request *handler.Request, response *handler.Response, user *model.User) (*model.Feed, error) {
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
feed, err := c.store.FeedByID(user.ID, feedID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if feed == nil {
|
||||
response.HTML().NotFound()
|
||||
return nil, errors.New("Feed not found")
|
||||
}
|
||||
|
||||
return feed, nil
|
||||
}
|
||||
|
||||
func (c *Controller) getFeedFormTemplateArgs(ctx *handler.Context, user *model.User, feed *model.Feed, feedForm *form.FeedForm) (tplParams, error) {
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
categories, err := c.store.Categories(user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if feedForm == nil {
|
||||
args["form"] = form.FeedForm{
|
||||
SiteURL: feed.SiteURL,
|
||||
FeedURL: feed.FeedURL,
|
||||
Title: feed.Title,
|
||||
ScraperRules: feed.ScraperRules,
|
||||
RewriteRules: feed.RewriteRules,
|
||||
Crawler: feed.Crawler,
|
||||
CategoryID: feed.Category.ID,
|
||||
}
|
||||
} else {
|
||||
args["form"] = feedForm
|
||||
}
|
||||
|
||||
args["categories"] = categories
|
||||
args["feed"] = feed
|
||||
args["menu"] = "feeds"
|
||||
return args, nil
|
||||
}
|
71
ui/feed_edit.go
Normal file
71
ui/feed_edit.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// EditFeed shows the form to modify a subscription.
|
||||
func (c *Controller) EditFeed(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
feedID, err := request.IntParam(r, "feedID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
feed, err := c.store.FeedByID(user.ID, feedID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if feed == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
categories, err := c.store.Categories(user.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
feedForm := form.FeedForm{
|
||||
SiteURL: feed.SiteURL,
|
||||
FeedURL: feed.FeedURL,
|
||||
Title: feed.Title,
|
||||
ScraperRules: feed.ScraperRules,
|
||||
RewriteRules: feed.RewriteRules,
|
||||
Crawler: feed.Crawler,
|
||||
CategoryID: feed.Category.ID,
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("form", feedForm)
|
||||
view.Set("categories", categories)
|
||||
view.Set("feed", feed)
|
||||
view.Set("menu", "feeds")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("edit_feed"))
|
||||
}
|
78
ui/feed_entries.go
Normal file
78
ui/feed_entries.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// ShowFeedEntries shows all entries for the given feed.
|
||||
func (c *Controller) ShowFeedEntries(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
feedID, err := request.IntParam(r, "feedID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
feed, err := c.store.FeedByID(user.ID, feedID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if feed == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
offset := request.QueryIntParam(r, "offset", 0)
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithFeedID(feed.ID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(nbItemsPerPage)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("feed", feed)
|
||||
view.Set("entries", entries)
|
||||
view.Set("total", count)
|
||||
view.Set("pagination", c.getPagination(route.Path(c.router, "feedEntries", "feedID", feed.ID), count, offset))
|
||||
view.Set("menu", "feeds")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("feed_entries"))
|
||||
}
|
36
ui/feed_icon.go
Normal file
36
ui/feed_icon.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
)
|
||||
|
||||
// ShowIcon shows the feed icon.
|
||||
func (c *Controller) ShowIcon(w http.ResponseWriter, r *http.Request) {
|
||||
iconID, err := request.IntParam(r, "iconID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
icon, err := c.store.IconByID(iconID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if icon == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
response.Cache(w, r, icon.MimeType, icon.Hash, icon.Content, 72*time.Hour)
|
||||
}
|
41
ui/feed_list.go
Normal file
41
ui/feed_list.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// ShowFeedsPage shows the page with all subscriptions.
|
||||
func (c *Controller) ShowFeedsPage(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
feeds, err := c.store.Feeds(user.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("feeds", feeds)
|
||||
view.Set("total", len(feeds))
|
||||
view.Set("menu", "feeds")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("feeds"))
|
||||
}
|
48
ui/feed_refresh.go
Normal file
48
ui/feed_refresh.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
)
|
||||
|
||||
// RefreshFeed refresh a subscription and redirect to the feed entries page.
|
||||
func (c *Controller) RefreshFeed(w http.ResponseWriter, r *http.Request) {
|
||||
feedID, err := request.IntParam(r, "feedID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.New(r)
|
||||
if err := c.feedHandler.RefreshFeed(ctx.UserID(), feedID); err != nil {
|
||||
logger.Error("[Controller:RefreshFeed] %v", err)
|
||||
}
|
||||
|
||||
response.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feedID))
|
||||
}
|
||||
|
||||
// RefreshAllFeeds refresh all feeds in the background for the current user.
|
||||
func (c *Controller) RefreshAllFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
userID := context.New(r).UserID()
|
||||
jobs, err := c.store.NewUserBatch(userID, c.store.CountFeeds(userID))
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
c.pool.Push(jobs)
|
||||
}()
|
||||
|
||||
response.Redirect(w, r, route.Path(c.router, "feeds"))
|
||||
}
|
32
ui/feed_remove.go
Normal file
32
ui/feed_remove.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
)
|
||||
|
||||
// RemoveFeed deletes a subscription from the database and redirect to the list of feeds page.
|
||||
func (c *Controller) RemoveFeed(w http.ResponseWriter, r *http.Request) {
|
||||
feedID, err := request.IntParam(r, "feedID")
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.New(r)
|
||||
if err := c.store.RemoveFeed(ctx.UserID(), feedID); err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(w, r, route.Path(c.router, "feeds"))
|
||||
}
|
80
ui/feed_update.go
Normal file
80
ui/feed_update.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// UpdateFeed update a subscription and redirect to the feed entries page.
|
||||
func (c *Controller) UpdateFeed(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
feedID, err := request.IntParam(r, "feedID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
feed, err := c.store.FeedByID(user.ID, feedID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if feed == nil {
|
||||
html.NotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
categories, err := c.store.Categories(user.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
feedForm := form.NewFeedForm(r)
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("form", feedForm)
|
||||
view.Set("categories", categories)
|
||||
view.Set("feed", feed)
|
||||
view.Set("menu", "feeds")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
if err := feedForm.ValidateModification(); err != nil {
|
||||
view.Set("errorMessage", err.Error())
|
||||
html.OK(w, view.Render("edit_feed"))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.UpdateFeed(feedForm.Merge(feed))
|
||||
if err != nil {
|
||||
logger.Error("[Controller:EditFeed] %v", err)
|
||||
view.Set("errorMessage", "Unable to update this feed.")
|
||||
html.OK(w, view.Render("edit_feed"))
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID))
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
)
|
||||
|
||||
// ShowHistoryPage renders the page with all read entries.
|
||||
func (c *Controller) ShowHistoryPage(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
offset := request.QueryIntegerParam("offset", 0)
|
||||
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithStatus(model.EntryStatusRead)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(nbItemsPerPage)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("history", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"entries": entries,
|
||||
"total": count,
|
||||
"pagination": c.getPagination(ctx.Route("history"), count, offset),
|
||||
"menu": "history",
|
||||
}))
|
||||
}
|
||||
|
||||
// FlushHistory changes all "read" items to "removed".
|
||||
func (c *Controller) FlushHistory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
err := c.store.FlushHistory(user.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(ctx.Route("history"))
|
||||
}
|
59
ui/history_entries.go
Normal file
59
ui/history_entries.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// ShowHistoryPage renders the page with all read entries.
|
||||
func (c *Controller) ShowHistoryPage(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
offset := request.QueryIntParam(r, "offset", 0)
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithStatus(model.EntryStatusRead)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(nbItemsPerPage)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("entries", entries)
|
||||
view.Set("total", count)
|
||||
view.Set("pagination", c.getPagination(route.Path(c.router, "history"), count, offset))
|
||||
view.Set("menu", "history")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("history"))
|
||||
}
|
25
ui/history_flush.go
Normal file
25
ui/history_flush.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
)
|
||||
|
||||
// FlushHistory changes all "read" items to "removed".
|
||||
func (c *Controller) FlushHistory(w http.ResponseWriter, r *http.Request) {
|
||||
err := c.store.FlushHistory(context.New(r).UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(w, r, route.Path(c.router, "history"))
|
||||
}
|
33
ui/icon.go
33
ui/icon.go
|
@ -1,33 +0,0 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
)
|
||||
|
||||
// ShowIcon shows the feed icon.
|
||||
func (c *Controller) ShowIcon(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
iconID, err := request.IntegerParam("iconID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
icon, err := c.store.IconByID(iconID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if icon == nil {
|
||||
response.HTML().NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
response.Cache(icon.MimeType, icon.Hash, icon.Content, 72*time.Hour)
|
||||
}
|
63
ui/integration_show.go
Normal file
63
ui/integration_show.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// ShowIntegrations renders the page with all external integrations.
|
||||
func (c *Controller) ShowIntegrations(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
integration, err := c.store.Integration(user.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
integrationForm := form.IntegrationForm{
|
||||
PinboardEnabled: integration.PinboardEnabled,
|
||||
PinboardToken: integration.PinboardToken,
|
||||
PinboardTags: integration.PinboardTags,
|
||||
PinboardMarkAsUnread: integration.PinboardMarkAsUnread,
|
||||
InstapaperEnabled: integration.InstapaperEnabled,
|
||||
InstapaperUsername: integration.InstapaperUsername,
|
||||
InstapaperPassword: integration.InstapaperPassword,
|
||||
FeverEnabled: integration.FeverEnabled,
|
||||
FeverUsername: integration.FeverUsername,
|
||||
FeverPassword: integration.FeverPassword,
|
||||
WallabagEnabled: integration.WallabagEnabled,
|
||||
WallabagURL: integration.WallabagURL,
|
||||
WallabagClientID: integration.WallabagClientID,
|
||||
WallabagClientSecret: integration.WallabagClientSecret,
|
||||
WallabagUsername: integration.WallabagUsername,
|
||||
WallabagPassword: integration.WallabagPassword,
|
||||
NunuxKeeperEnabled: integration.NunuxKeeperEnabled,
|
||||
NunuxKeeperURL: integration.NunuxKeeperURL,
|
||||
NunuxKeeperAPIKey: integration.NunuxKeeperAPIKey,
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("form", integrationForm)
|
||||
view.Set("menu", "settings")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("integrations"))
|
||||
}
|
60
ui/integration_update.go
Normal file
60
ui/integration_update.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
)
|
||||
|
||||
// UpdateIntegration updates integration settings.
|
||||
func (c *Controller) UpdateIntegration(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
sess := session.New(c.store, ctx)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
integration, err := c.store.Integration(user.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
integrationForm := form.NewIntegrationForm(r)
|
||||
integrationForm.Merge(integration)
|
||||
|
||||
if integration.FeverUsername != "" && c.store.HasDuplicateFeverUsername(user.ID, integration.FeverUsername) {
|
||||
sess.NewFlashErrorMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("There is already someone else with the same Fever username!"))
|
||||
response.Redirect(w, r, route.Path(c.router, "integrations"))
|
||||
return
|
||||
}
|
||||
|
||||
if integration.FeverEnabled {
|
||||
integration.FeverToken = fmt.Sprintf("%x", md5.Sum([]byte(integration.FeverUsername+":"+integration.FeverPassword)))
|
||||
} else {
|
||||
integration.FeverToken = ""
|
||||
}
|
||||
|
||||
err = c.store.UpdateIntegration(integration)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess.NewFlashMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("Preferences saved!"))
|
||||
response.Redirect(w, r, route.Path(c.router, "integrations"))
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
)
|
||||
|
||||
// ShowIntegrations renders the page with all external integrations.
|
||||
func (c *Controller) ShowIntegrations(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
integration, err := c.store.Integration(user.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("integrations", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"menu": "settings",
|
||||
"form": form.IntegrationForm{
|
||||
PinboardEnabled: integration.PinboardEnabled,
|
||||
PinboardToken: integration.PinboardToken,
|
||||
PinboardTags: integration.PinboardTags,
|
||||
PinboardMarkAsUnread: integration.PinboardMarkAsUnread,
|
||||
InstapaperEnabled: integration.InstapaperEnabled,
|
||||
InstapaperUsername: integration.InstapaperUsername,
|
||||
InstapaperPassword: integration.InstapaperPassword,
|
||||
FeverEnabled: integration.FeverEnabled,
|
||||
FeverUsername: integration.FeverUsername,
|
||||
FeverPassword: integration.FeverPassword,
|
||||
WallabagEnabled: integration.WallabagEnabled,
|
||||
WallabagURL: integration.WallabagURL,
|
||||
WallabagClientID: integration.WallabagClientID,
|
||||
WallabagClientSecret: integration.WallabagClientSecret,
|
||||
WallabagUsername: integration.WallabagUsername,
|
||||
WallabagPassword: integration.WallabagPassword,
|
||||
NunuxKeeperEnabled: integration.NunuxKeeperEnabled,
|
||||
NunuxKeeperURL: integration.NunuxKeeperURL,
|
||||
NunuxKeeperAPIKey: integration.NunuxKeeperAPIKey,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
// UpdateIntegration updates integration settings.
|
||||
func (c *Controller) UpdateIntegration(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
integration, err := c.store.Integration(user.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
integrationForm := form.NewIntegrationForm(request.Request())
|
||||
integrationForm.Merge(integration)
|
||||
|
||||
if integration.FeverUsername != "" && c.store.HasDuplicateFeverUsername(user.ID, integration.FeverUsername) {
|
||||
ctx.SetFlashErrorMessage(ctx.Translate("There is already someone else with the same Fever username!"))
|
||||
response.Redirect(ctx.Route("integrations"))
|
||||
return
|
||||
}
|
||||
|
||||
if integration.FeverEnabled {
|
||||
integration.FeverToken = fmt.Sprintf("%x", md5.Sum([]byte(integration.FeverUsername+":"+integration.FeverPassword)))
|
||||
} else {
|
||||
integration.FeverToken = ""
|
||||
}
|
||||
|
||||
err = c.store.UpdateIntegration(integration)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(ctx.Route("integrations"))
|
||||
}
|
80
ui/login.go
80
ui/login.go
|
@ -1,80 +0,0 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/miniflux/miniflux/http/cookie"
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
|
||||
"github.com/tomasen/realip"
|
||||
)
|
||||
|
||||
// ShowLoginPage shows the login form.
|
||||
func (c *Controller) ShowLoginPage(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
if ctx.IsAuthenticated() {
|
||||
response.Redirect(ctx.Route("unread"))
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("login", ctx.UserLanguage(), tplParams{
|
||||
"csrf": ctx.CSRF(),
|
||||
})
|
||||
}
|
||||
|
||||
// CheckLogin validates the username/password and redirects the user to the unread page.
|
||||
func (c *Controller) CheckLogin(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
authForm := form.NewAuthForm(request.Request())
|
||||
tplParams := tplParams{
|
||||
"errorMessage": "Invalid username or password.",
|
||||
"csrf": ctx.CSRF(),
|
||||
"form": authForm,
|
||||
}
|
||||
|
||||
if err := authForm.Validate(); err != nil {
|
||||
logger.Error("[Controller:CheckLogin] %v", err)
|
||||
response.HTML().Render("login", ctx.UserLanguage(), tplParams)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.CheckPassword(authForm.Username, authForm.Password); err != nil {
|
||||
logger.Error("[Controller:CheckLogin] %v", err)
|
||||
response.HTML().Render("login", ctx.UserLanguage(), tplParams)
|
||||
return
|
||||
}
|
||||
|
||||
sessionToken, err := c.store.CreateUserSession(
|
||||
authForm.Username,
|
||||
request.Request().UserAgent(),
|
||||
realip.RealIP(request.Request()),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("[Controller:CheckLogin] username=%s just logged in", authForm.Username)
|
||||
|
||||
response.SetCookie(cookie.New(cookie.CookieUserSessionID, sessionToken, c.cfg.IsHTTPS, c.cfg.BasePath()))
|
||||
response.Redirect(ctx.Route("unread"))
|
||||
}
|
||||
|
||||
// Logout destroy the session and redirects the user to the login page.
|
||||
func (c *Controller) Logout(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
if err := c.store.UpdateSessionField(ctx.SessionID(), "language", user.Language); err != nil {
|
||||
logger.Error("[Controller:Logout] %v", err)
|
||||
}
|
||||
|
||||
if err := c.store.RemoveUserSessionByToken(user.ID, ctx.UserSessionToken()); err != nil {
|
||||
logger.Error("[Controller:Logout] %v", err)
|
||||
}
|
||||
|
||||
response.SetCookie(cookie.Expired(cookie.CookieUserSessionID, c.cfg.IsHTTPS, c.cfg.BasePath()))
|
||||
response.Redirect(ctx.Route("login"))
|
||||
}
|
66
ui/login_check.go
Normal file
66
ui/login_check.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/cookie"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
"github.com/tomasen/realip"
|
||||
)
|
||||
|
||||
// CheckLogin validates the username/password and redirects the user to the unread page.
|
||||
func (c *Controller) CheckLogin(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
sess := session.New(c.store, ctx)
|
||||
|
||||
authForm := form.NewAuthForm(r)
|
||||
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("errorMessage", "Invalid username or password.")
|
||||
view.Set("form", authForm)
|
||||
|
||||
if err := authForm.Validate(); err != nil {
|
||||
logger.Error("[Controller:CheckLogin] %v", err)
|
||||
html.OK(w, view.Render("login"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.CheckPassword(authForm.Username, authForm.Password); err != nil {
|
||||
logger.Error("[Controller:CheckLogin] %v", err)
|
||||
html.OK(w, view.Render("login"))
|
||||
return
|
||||
}
|
||||
|
||||
sessionToken, userID, err := c.store.CreateUserSession(authForm.Username, r.UserAgent(), realip.RealIP(r))
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("[Controller:CheckLogin] username=%s just logged in", authForm.Username)
|
||||
c.store.SetLastLogin(userID)
|
||||
|
||||
userLanguage, err := c.store.UserLanguage(userID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess.SetLanguage(userLanguage)
|
||||
|
||||
http.SetCookie(w, cookie.New(
|
||||
cookie.CookieUserSessionID,
|
||||
sessionToken,
|
||||
c.cfg.IsHTTPS,
|
||||
c.cfg.BasePath(),
|
||||
))
|
||||
|
||||
response.Redirect(w, r, route.Path(c.router, "unread"))
|
||||
}
|
29
ui/login_show.go
Normal file
29
ui/login_show.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// ShowLoginPage shows the login form.
|
||||
func (c *Controller) ShowLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
if ctx.IsAuthenticated() {
|
||||
response.Redirect(w, r, route.Path(c.router, "unread"))
|
||||
return
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
html.OK(w, view.Render("login"))
|
||||
}
|
43
ui/logout.go
Normal file
43
ui/logout.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/cookie"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
)
|
||||
|
||||
// Logout destroy the session and redirects the user to the login page.
|
||||
func (c *Controller) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
sess := session.New(c.store, ctx)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess.SetLanguage(user.Language)
|
||||
|
||||
if err := c.store.RemoveUserSessionByToken(user.ID, ctx.UserSessionToken()); err != nil {
|
||||
logger.Error("[Controller:Logout] %v", err)
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie.Expired(
|
||||
cookie.CookieUserSessionID,
|
||||
c.cfg.IsHTTPS,
|
||||
c.cfg.BasePath(),
|
||||
))
|
||||
|
||||
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||
}
|
155
ui/oauth2.go
155
ui/oauth2.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// 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(),
|
||||
|
|
128
ui/oauth2_callback.go
Normal file
128
ui/oauth2_callback.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/cookie"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
|
||||
"github.com/tomasen/realip"
|
||||
)
|
||||
|
||||
// OAuth2Callback receives the authorization code and create a new session.
|
||||
func (c *Controller) OAuth2Callback(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
sess := session.New(c.store, ctx)
|
||||
|
||||
provider := request.Param(r, "provider", "")
|
||||
if provider == "" {
|
||||
logger.Error("[OAuth2] Invalid or missing provider")
|
||||
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||
return
|
||||
}
|
||||
|
||||
code := request.QueryParam(r, "code", "")
|
||||
if code == "" {
|
||||
logger.Error("[OAuth2] No code received on callback")
|
||||
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||
return
|
||||
}
|
||||
|
||||
state := request.QueryParam(r, "state", "")
|
||||
if state == "" || state != ctx.OAuth2State() {
|
||||
logger.Error(`[OAuth2] Invalid state value: got "%s" instead of "%s"`, state, ctx.OAuth2State())
|
||||
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||
return
|
||||
}
|
||||
|
||||
authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
|
||||
if err != nil {
|
||||
logger.Error("[OAuth2] %v", err)
|
||||
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||
return
|
||||
}
|
||||
|
||||
profile, err := authProvider.GetProfile(code)
|
||||
if err != nil {
|
||||
logger.Error("[OAuth2] %v", err)
|
||||
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.IsAuthenticated() {
|
||||
user, err := c.store.UserByExtraField(profile.Key, profile.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
logger.Error("[OAuth2] User #%d cannot be associated because %s is already associated", ctx.UserID(), user.Username)
|
||||
sess.NewFlashErrorMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("There is already someone associated with this provider!"))
|
||||
response.Redirect(w, r, route.Path(c.router, "settings"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.UpdateExtraField(ctx.UserID(), profile.Key, profile.ID); err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess.NewFlashMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("Your external account is now linked!"))
|
||||
response.Redirect(w, r, route.Path(c.router, "settings"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := c.store.UserByExtraField(profile.Key, profile.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
if !c.cfg.IsOAuth2UserCreationAllowed() {
|
||||
html.Forbidden(w)
|
||||
return
|
||||
}
|
||||
|
||||
user = model.NewUser()
|
||||
user.Username = profile.Username
|
||||
user.IsAdmin = false
|
||||
user.Extra[profile.Key] = profile.ID
|
||||
|
||||
if err := c.store.CreateUser(user); err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sessionToken, _, err := c.store.CreateUserSession(user.Username, r.UserAgent(), realip.RealIP(r))
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("[Controller:OAuth2Callback] username=%s just logged in", user.Username)
|
||||
c.store.SetLastLogin(user.ID)
|
||||
sess.SetLanguage(user.Language)
|
||||
|
||||
http.SetCookie(w, cookie.New(
|
||||
cookie.CookieUserSessionID,
|
||||
sessionToken,
|
||||
c.cfg.IsHTTPS,
|
||||
c.cfg.BasePath(),
|
||||
))
|
||||
|
||||
response.Redirect(w, r, route.Path(c.router, "unread"))
|
||||
}
|
38
ui/oauth2_redirect.go
Normal file
38
ui/oauth2_redirect.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
)
|
||||
|
||||
// OAuth2Redirect redirects the user to the consent page to ask for permission.
|
||||
func (c *Controller) OAuth2Redirect(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
sess := session.New(c.store, ctx)
|
||||
|
||||
provider := request.Param(r, "provider", "")
|
||||
if provider == "" {
|
||||
logger.Error("[OAuth2] Invalid or missing provider: %s", provider)
|
||||
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||
return
|
||||
}
|
||||
|
||||
authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
|
||||
if err != nil {
|
||||
logger.Error("[OAuth2] %v", err)
|
||||
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(w, r, authProvider.GetRedirectURL(sess.NewOAuth2State()))
|
||||
}
|
45
ui/oauth2_unlink.go
Normal file
45
ui/oauth2_unlink.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
)
|
||||
|
||||
// OAuth2Unlink unlink an account from the external provider.
|
||||
func (c *Controller) OAuth2Unlink(w http.ResponseWriter, r *http.Request) {
|
||||
provider := request.Param(r, "provider", "")
|
||||
if provider == "" {
|
||||
logger.Info("[OAuth2] Invalid or missing provider")
|
||||
response.Redirect(w, r, route.Path(c.router, "login"))
|
||||
return
|
||||
}
|
||||
|
||||
authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
|
||||
if err != nil {
|
||||
logger.Error("[OAuth2] %v", err)
|
||||
response.Redirect(w, r, route.Path(c.router, "settings"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.New(r)
|
||||
if err := c.store.RemoveExtraField(ctx.UserID(), authProvider.GetUserExtraKey()); err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
sess.NewFlashMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("Your external account is now dissociated!"))
|
||||
response.Redirect(w, r, route.Path(c.router, "settings"))
|
||||
return
|
||||
}
|
72
ui/opml.go
72
ui/opml.go
|
@ -1,72 +0,0 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/reader/opml"
|
||||
)
|
||||
|
||||
// Export generates the OPML file.
|
||||
func (c *Controller) Export(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
opml, err := opml.NewHandler(c.store).Export(user.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.XML().Download("feeds.opml", opml)
|
||||
}
|
||||
|
||||
// Import shows the import form.
|
||||
func (c *Controller) Import(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("import", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"menu": "feeds",
|
||||
}))
|
||||
}
|
||||
|
||||
// UploadOPML handles OPML file importation.
|
||||
func (c *Controller) UploadOPML(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
file, fileHeader, err := request.File("file")
|
||||
if err != nil {
|
||||
logger.Error("[Controller:UploadOPML] %v", err)
|
||||
response.Redirect(ctx.Route("import"))
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
user := ctx.LoggedUser()
|
||||
logger.Info(
|
||||
"[Controller:UploadOPML] User #%d uploaded this file: %s (%d bytes)",
|
||||
user.ID,
|
||||
fileHeader.Filename,
|
||||
fileHeader.Size,
|
||||
)
|
||||
|
||||
if impErr := opml.NewHandler(c.store).Import(user.ID, file); impErr != nil {
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("import", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"errorMessage": impErr,
|
||||
"menu": "feeds",
|
||||
}))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(ctx.Route("feeds"))
|
||||
}
|
26
ui/opml_export.go
Normal file
26
ui/opml_export.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/response/xml"
|
||||
"github.com/miniflux/miniflux/reader/opml"
|
||||
)
|
||||
|
||||
// Export generates the OPML file.
|
||||
func (c *Controller) Export(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
opml, err := opml.NewHandler(c.store).Export(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
xml.Attachment(w, "feeds.opml", opml)
|
||||
}
|
33
ui/opml_import.go
Normal file
33
ui/opml_import.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// Import shows the import form.
|
||||
func (c *Controller) Import(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("menu", "feeds")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("import"))
|
||||
}
|
64
ui/opml_upload.go
Normal file
64
ui/opml_upload.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/reader/opml"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// UploadOPML handles OPML file importation.
|
||||
func (c *Controller) UploadOPML(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
file, fileHeader, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
logger.Error("[Controller:UploadOPML] %v", err)
|
||||
response.Redirect(w, r, route.Path(c.router, "import"))
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
logger.Info(
|
||||
"[Controller:UploadOPML] User #%d uploaded this file: %s (%d bytes)",
|
||||
user.ID,
|
||||
fileHeader.Filename,
|
||||
fileHeader.Size,
|
||||
)
|
||||
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
view.Set("menu", "feeds")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
if fileHeader.Size == 0 {
|
||||
view.Set("errorMessage", "This file is empty")
|
||||
html.OK(w, view.Render("import"))
|
||||
return
|
||||
}
|
||||
|
||||
if impErr := opml.NewHandler(c.store).Import(user.ID, file); impErr != nil {
|
||||
view.Set("errorMessage", impErr)
|
||||
html.OK(w, view.Render("import"))
|
||||
return
|
||||
}
|
||||
|
||||
response.Redirect(w, r, route.Path(c.router, "feeds"))
|
||||
}
|
|
@ -12,14 +12,15 @@ import (
|
|||
"github.com/miniflux/miniflux/model"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
23
ui/proxy.go
23
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)
|
||||
}
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
)
|
||||
|
||||
// ShowSessions shows the list of active user sessions.
|
||||
func (c *Controller) ShowSessions(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
sessions, err := c.store.UserSessions(user.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
sessions.UseTimezone(user.Timezone)
|
||||
response.HTML().Render("sessions", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"sessions": sessions,
|
||||
"currentSessionToken": ctx.UserSessionToken(),
|
||||
"menu": "settings",
|
||||
}))
|
||||
}
|
||||
|
||||
// RemoveSession remove a user session.
|
||||
func (c *Controller) RemoveSession(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
sessionID, err := request.IntegerParam("sessionID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.RemoveUserSessionByID(user.ID, sessionID)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:RemoveSession] %v", err)
|
||||
}
|
||||
|
||||
response.Redirect(ctx.Route("sessions"))
|
||||
}
|
62
ui/session/session.go
Normal file
62
ui/session/session.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package session
|
||||
|
||||
import (
|
||||
"github.com/miniflux/miniflux/crypto"
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
)
|
||||
|
||||
// Session handles session data.
|
||||
type Session struct {
|
||||
store *storage.Storage
|
||||
ctx *context.Context
|
||||
}
|
||||
|
||||
// NewOAuth2State generates a new OAuth2 state and stores the value into the database.
|
||||
func (s *Session) NewOAuth2State() string {
|
||||
state := crypto.GenerateRandomString(32)
|
||||
s.store.UpdateSessionField(s.ctx.SessionID(), "oauth2_state", state)
|
||||
return state
|
||||
}
|
||||
|
||||
// NewFlashMessage creates a new flash message.
|
||||
func (s *Session) NewFlashMessage(message string) {
|
||||
s.store.UpdateSessionField(s.ctx.SessionID(), "flash_message", message)
|
||||
}
|
||||
|
||||
// FlashMessage returns the current flash message if any.
|
||||
func (s *Session) FlashMessage() string {
|
||||
message := s.ctx.FlashMessage()
|
||||
if message != "" {
|
||||
s.store.UpdateSessionField(s.ctx.SessionID(), "flash_message", "")
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
// NewFlashErrorMessage creates a new flash error message.
|
||||
func (s *Session) NewFlashErrorMessage(message string) {
|
||||
s.store.UpdateSessionField(s.ctx.SessionID(), "flash_error_message", message)
|
||||
}
|
||||
|
||||
// FlashErrorMessage returns the last flash error message if any.
|
||||
func (s *Session) FlashErrorMessage() string {
|
||||
message := s.ctx.FlashErrorMessage()
|
||||
if message != "" {
|
||||
s.store.UpdateSessionField(s.ctx.SessionID(), "flash_error_message", "")
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
// SetLanguage updates language field in session.
|
||||
func (s *Session) SetLanguage(language string) {
|
||||
s.store.UpdateSessionField(s.ctx.SessionID(), "language", language)
|
||||
}
|
||||
|
||||
// New returns a new session handler.
|
||||
func New(store *storage.Storage, ctx *context.Context) *Session {
|
||||
return &Session{store, ctx}
|
||||
}
|
44
ui/session_list.go
Normal file
44
ui/session_list.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
)
|
||||
|
||||
// ShowSessions shows the list of active user sessions.
|
||||
func (c *Controller) ShowSessions(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sessions, err := c.store.UserSessions(user.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
sessions.UseTimezone(user.Timezone)
|
||||
|
||||
view.Set("currentSessionToken", ctx.UserSessionToken())
|
||||
view.Set("sessions", sessions)
|
||||
view.Set("menu", "settings")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("sessions"))
|
||||
}
|
34
ui/session_remove.go
Normal file
34
ui/session_remove.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/request"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
)
|
||||
|
||||
// RemoveSession remove a user session.
|
||||
func (c *Controller) RemoveSession(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
|
||||
sessionID, err := request.IntParam(r, "sessionID")
|
||||
if err != nil {
|
||||
html.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.RemoveUserSessionByID(ctx.UserID(), sessionID)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:RemoveSession] %v", err)
|
||||
}
|
||||
|
||||
response.Redirect(w, r, route.Path(c.router, "sessions"))
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
)
|
||||
|
||||
// ShowSettings shows the settings page.
|
||||
func (c *Controller) ShowSettings(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
args, err := c.getSettingsFormTemplateArgs(ctx, user, nil)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("settings", ctx.UserLanguage(), args)
|
||||
}
|
||||
|
||||
// UpdateSettings update the settings.
|
||||
func (c *Controller) UpdateSettings(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
|
||||
settingsForm := form.NewSettingsForm(request.Request())
|
||||
args, err := c.getSettingsFormTemplateArgs(ctx, user, settingsForm)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := settingsForm.Validate(); err != nil {
|
||||
response.HTML().Render("settings", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"form": settingsForm,
|
||||
"errorMessage": err.Error(),
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
if c.store.AnotherUserExists(user.ID, settingsForm.Username) {
|
||||
response.HTML().Render("settings", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"form": settingsForm,
|
||||
"errorMessage": "This user already exists.",
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.UpdateUser(settingsForm.Merge(user))
|
||||
if err != nil {
|
||||
logger.Error("[Controller:UpdateSettings] %v", err)
|
||||
response.HTML().Render("settings", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"form": settingsForm,
|
||||
"errorMessage": "Unable to update this user.",
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetFlashMessage(ctx.Translate("Preferences saved!"))
|
||||
response.Redirect(ctx.Route("settings"))
|
||||
}
|
||||
|
||||
func (c *Controller) getSettingsFormTemplateArgs(ctx *handler.Context, user *model.User, settingsForm *form.SettingsForm) (tplParams, error) {
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
return args, err
|
||||
}
|
||||
|
||||
if settingsForm == nil {
|
||||
args["form"] = form.SettingsForm{
|
||||
Username: user.Username,
|
||||
Theme: user.Theme,
|
||||
Language: user.Language,
|
||||
Timezone: user.Timezone,
|
||||
EntryDirection: user.EntryDirection,
|
||||
}
|
||||
} else {
|
||||
args["form"] = settingsForm
|
||||
}
|
||||
|
||||
args["menu"] = "settings"
|
||||
args["themes"] = model.Themes()
|
||||
args["languages"] = locale.AvailableLanguages()
|
||||
args["timezones"], err = c.store.Timezones()
|
||||
if err != nil {
|
||||
return args, err
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
54
ui/settings_show.go
Normal file
54
ui/settings_show.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// ShowSettings shows the settings page.
|
||||
func (c *Controller) ShowSettings(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
settingsForm := form.SettingsForm{
|
||||
Username: user.Username,
|
||||
Theme: user.Theme,
|
||||
Language: user.Language,
|
||||
Timezone: user.Timezone,
|
||||
EntryDirection: user.EntryDirection,
|
||||
}
|
||||
|
||||
timezones, err := c.store.Timezones()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
view.Set("form", settingsForm)
|
||||
view.Set("themes", model.Themes())
|
||||
view.Set("languages", locale.AvailableLanguages())
|
||||
view.Set("timezones", timezones)
|
||||
view.Set("menu", "settings")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
html.OK(w, view.Render("settings"))
|
||||
}
|
72
ui/settings_update.go
Normal file
72
ui/settings_update.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/response"
|
||||
"github.com/miniflux/miniflux/http/response/html"
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/ui/form"
|
||||
"github.com/miniflux/miniflux/ui/session"
|
||||
"github.com/miniflux/miniflux/ui/view"
|
||||
)
|
||||
|
||||
// UpdateSettings update the settings.
|
||||
func (c *Controller) UpdateSettings(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.New(r)
|
||||
sess := session.New(c.store, ctx)
|
||||
view := view.New(c.tpl, ctx, sess)
|
||||
|
||||
user, err := c.store.UserByID(ctx.UserID())
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
timezones, err := c.store.Timezones()
|
||||
if err != nil {
|
||||
html.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
settingsForm := form.NewSettingsForm(r)
|
||||
|
||||
view.Set("form", settingsForm)
|
||||
view.Set("themes", model.Themes())
|
||||
view.Set("languages", locale.AvailableLanguages())
|
||||
view.Set("timezones", timezones)
|
||||
view.Set("menu", "settings")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
|
||||
|
||||
if err := settingsForm.Validate(); err != nil {
|
||||
view.Set("errorMessage", err.Error())
|
||||
html.OK(w, view.Render("settings"))
|
||||
return
|
||||
}
|
||||
|
||||
if c.store.AnotherUserExists(user.ID, settingsForm.Username) {
|
||||
view.Set("errorMessage", "This user already exists.")
|
||||
html.OK(w, view.Render("settings"))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.UpdateUser(settingsForm.Merge(user))
|
||||
if err != nil {
|
||||
logger.Error("[Controller:UpdateSettings] %v", err)
|
||||
view.Set("errorMessage", "Unable to update this user.")
|
||||
html.OK(w, view.Render("settings"))
|
||||
return
|
||||
}
|
||||
|
||||
sess.NewFlashMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("Preferences saved!"))
|
||||
response.Redirect(w, r, route.Path(c.router, "settings"))
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
)
|
||||
|
||||
// ShowStarredPage renders the page with all starred entries.
|
||||
func (c *Controller) ShowStarredPage(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
offset := request.QueryIntegerParam("offset", 0)
|
||||
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithStarred()
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(nbItemsPerPage)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.HTML().Render("starred", ctx.UserLanguage(), args.Merge(tplParams{
|
||||
"entries": entries,
|
||||
"total": count,
|
||||
"pagination": c.getPagination(ctx.Route("starred"), count, offset),
|
||||
"menu": "starred",
|
||||
}))
|
||||
}
|
||||
|
||||
// ToggleBookmark handles Ajax request to toggle bookmark value.
|
||||
func (c *Controller) ToggleBookmark(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
if err != nil {
|
||||
response.HTML().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.ToggleBookmark(user.ID, entryID); err != nil {
|
||||
logger.Error("[Controller:UpdateEntryStatus] %v", err)
|
||||
response.JSON().ServerError(nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard("OK")
|
||||
}
|
97
ui/static.go
97
ui/static.go
|
@ -1,97 +0,0 @@
|
|||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/ui/static"
|
||||
)
|
||||
|
||||
// Stylesheet renders the CSS.
|
||||
func (c *Controller) Stylesheet(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
stylesheet := request.StringParam("name", "white")
|
||||
body := static.Stylesheets["common"]
|
||||
etag := static.StylesheetsChecksums["common"]
|
||||
|
||||
if theme, found := static.Stylesheets[stylesheet]; found {
|
||||
body += theme
|
||||
etag += static.StylesheetsChecksums[stylesheet]
|
||||
}
|
||||
|
||||
response.Cache("text/css; charset=utf-8", etag, []byte(body), 48*time.Hour)
|
||||
}
|
||||
|
||||
// Javascript renders application client side code.
|
||||
func (c *Controller) Javascript(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
response.Cache("text/javascript; charset=utf-8", static.JavascriptChecksums["app"], []byte(static.Javascript["app"]), 48*time.Hour)
|
||||
}
|
||||
|
||||
// Favicon renders the application favicon.
|
||||
func (c *Controller) Favicon(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
blob, err := base64.StdEncoding.DecodeString(static.Binaries["favicon.ico"])
|
||||
if err != nil {
|
||||
logger.Error("[Controller:Favicon] %v", err)
|
||||
response.HTML().NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
response.Cache("image/x-icon", static.BinariesChecksums["favicon.ico"], blob, 48*time.Hour)
|
||||
}
|
||||
|
||||
// AppIcon returns application icons.
|
||||
func (c *Controller) AppIcon(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
filename := request.StringParam("filename", "favicon.png")
|
||||
encodedBlob, found := static.Binaries[filename]
|
||||
if !found {
|
||||
logger.Info("[Controller:AppIcon] This icon doesn't exists: %s", filename)
|
||||
response.HTML().NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
blob, err := base64.StdEncoding.DecodeString(encodedBlob)
|
||||
if err != nil {
|
||||
logger.Error("[Controller:AppIcon] %v", err)
|
||||
response.HTML().NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
response.Cache("image/png", static.BinariesChecksums[filename], blob, 48*time.Hour)
|
||||
}
|
||||
|
||||
// WebManifest renders web manifest file.
|
||||
func (c *Controller) WebManifest(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
type webManifestIcon struct {
|
||||
Source string `json:"src"`
|
||||
Sizes string `json:"sizes"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type webManifest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
ShortName string `json:"short_name"`
|
||||
StartURL string `json:"start_url"`
|
||||
Icons []webManifestIcon `json:"icons"`
|
||||
Display string `json:"display"`
|
||||
}
|
||||
|
||||
manifest := &webManifest{
|
||||
Name: "Miniflux",
|
||||
ShortName: "Miniflux",
|
||||
Description: "Minimalist Feed Reader",
|
||||
Display: "minimal-ui",
|
||||
StartURL: ctx.Route("unread"),
|
||||
Icons: []webManifestIcon{
|
||||
webManifestIcon{Source: ctx.Route("appIcon", "filename", "touch-icon-ipad-retina.png"), Sizes: "144x144", Type: "image/png"},
|
||||
webManifestIcon{Source: ctx.Route("appIcon", "filename", "touch-icon-iphone-retina.png"), Sizes: "114x114", Type: "image/png"},
|
||||
},
|
||||
}
|
||||
|
||||
response.JSON().Standard(manifest)
|
||||
}
|
37
ui/static_app_icon.go
Normal file
37
ui/static_app_icon.go
Normal file
|
@ -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)
|
||||
}
|
28
ui/static_favicon.go
Normal file
28
ui/static_favicon.go
Normal file
|
@ -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)
|
||||
}
|
18
ui/static_javascript.go
Normal file
18
ui/static_javascript.go
Normal file
|
@ -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)
|
||||
}
|
44
ui/static_manifest.go
Normal file
44
ui/static_manifest.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/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)
|
||||
}
|
28
ui/static_stylesheet.go
Normal file
28
ui/static_stylesheet.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
40
ui/subscription_add.go
Normal file
40
ui/subscription_add.go
Normal file
|
@ -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"))
|
||||
}
|
45
ui/subscription_bookmarklet.go
Normal file
45
ui/subscription_bookmarklet.go
Normal file
|
@ -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"))
|
||||
}
|
59
ui/subscription_choose.go
Normal file
59
ui/subscription_choose.go
Normal file
|
@ -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))
|
||||
}
|
89
ui/subscription_submit.go
Normal file
89
ui/subscription_submit.go
Normal file
|
@ -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"))
|
||||
}
|
||||
}
|
59
ui/unread.go
59
ui/unread.go
|
@ -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"))
|
||||
}
|
63
ui/unread_entries.go
Normal file
63
ui/unread_entries.go
Normal file
|
@ -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"))
|
||||
}
|
23
ui/unread_mark_all_read.go
Normal file
23
ui/unread_mark_all_read.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package 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"))
|
||||
}
|
239
ui/user.go
239
ui/user.go
|
@ -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
|
||||
}
|
40
ui/user_create.go
Normal file
40
ui/user_create.go
Normal file
|
@ -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"))
|
||||
}
|
64
ui/user_edit.go
Normal file
64
ui/user_edit.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/context"
|
||||
"github.com/miniflux/miniflux/http/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"))
|
||||
}
|
47
ui/user_list.go
Normal file
47
ui/user_list.go
Normal file
|
@ -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"))
|
||||
}
|
55
ui/user_remove.go
Normal file
55
ui/user_remove.go
Normal file
|
@ -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"))
|
||||
}
|
65
ui/user_save.go
Normal file
65
ui/user_save.go
Normal file
|
@ -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"))
|
||||
}
|
84
ui/user_update.go
Normal file
84
ui/user_update.go
Normal file
|
@ -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"))
|
||||
}
|
39
ui/view/view.go
Normal file
39
ui/view/view.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue