Refactor feed validator

This commit is contained in:
Frédéric Guillot 2021-01-04 13:49:28 -08:00 committed by fguillot
parent b35fece3d5
commit 806b9545a9
32 changed files with 588 additions and 521 deletions

View file

@ -36,7 +36,7 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
sr.HandleFunc("/categories/{categoryID}", handler.updateCategory).Methods(http.MethodPut) sr.HandleFunc("/categories/{categoryID}", handler.updateCategory).Methods(http.MethodPut)
sr.HandleFunc("/categories/{categoryID}", handler.removeCategory).Methods(http.MethodDelete) sr.HandleFunc("/categories/{categoryID}", handler.removeCategory).Methods(http.MethodDelete)
sr.HandleFunc("/categories/{categoryID}/mark-all-as-read", handler.markCategoryAsRead).Methods(http.MethodPut) sr.HandleFunc("/categories/{categoryID}/mark-all-as-read", handler.markCategoryAsRead).Methods(http.MethodPut)
sr.HandleFunc("/discover", handler.getSubscriptions).Methods(http.MethodPost) sr.HandleFunc("/discover", handler.discoverSubscriptions).Methods(http.MethodPost)
sr.HandleFunc("/feeds", handler.createFeed).Methods(http.MethodPost) sr.HandleFunc("/feeds", handler.createFeed).Methods(http.MethodPost)
sr.HandleFunc("/feeds", handler.getFeeds).Methods(http.MethodGet) sr.HandleFunc("/feeds", handler.getFeeds).Methods(http.MethodGet)
sr.HandleFunc("/feeds/refresh", handler.refreshAllFeeds).Methods(http.MethodPut) sr.HandleFunc("/feeds/refresh", handler.refreshAllFeeds).Methods(http.MethodPut)

View file

@ -5,60 +5,32 @@
package api // import "miniflux.app/api" package api // import "miniflux.app/api"
import ( import (
"errors" json_parser "encoding/json"
"net/http" "net/http"
"time" "time"
"miniflux.app/http/request" "miniflux.app/http/request"
"miniflux.app/http/response/json" "miniflux.app/http/response/json"
"miniflux.app/model"
feedHandler "miniflux.app/reader/handler" feedHandler "miniflux.app/reader/handler"
"miniflux.app/validator"
) )
func (h *handler) createFeed(w http.ResponseWriter, r *http.Request) { func (h *handler) createFeed(w http.ResponseWriter, r *http.Request) {
feedInfo, err := decodeFeedCreationRequest(r.Body) userID := request.UserID(r)
if err != nil {
var feedCreationRequest model.FeedCreationRequest
if err := json_parser.NewDecoder(r.Body).Decode(&feedCreationRequest); err != nil {
json.BadRequest(w, r, err) json.BadRequest(w, r, err)
return return
} }
if feedInfo.FeedURL == "" { if validationErr := validator.ValidateFeedCreation(h.store, userID, &feedCreationRequest); validationErr != nil {
json.BadRequest(w, r, errors.New("The feed_url is required")) json.BadRequest(w, r, validationErr.Error())
return return
} }
if feedInfo.CategoryID <= 0 { feed, err := feedHandler.CreateFeed(h.store, userID, &feedCreationRequest)
json.BadRequest(w, r, errors.New("The category_id is required"))
return
}
userID := request.UserID(r)
if h.store.FeedURLExists(userID, feedInfo.FeedURL) {
json.BadRequest(w, r, errors.New("This feed_url already exists"))
return
}
if !h.store.CategoryIDExists(userID, feedInfo.CategoryID) {
json.BadRequest(w, r, errors.New("This category_id doesn't exists or doesn't belongs to this user"))
return
}
feed, err := feedHandler.CreateFeed(h.store, &feedHandler.FeedCreationArgs{
UserID: userID,
CategoryID: feedInfo.CategoryID,
FeedURL: feedInfo.FeedURL,
UserAgent: feedInfo.UserAgent,
Username: feedInfo.Username,
Password: feedInfo.Password,
Crawler: feedInfo.Crawler,
Disabled: feedInfo.Disabled,
IgnoreHTTPCache: feedInfo.IgnoreHTTPCache,
FetchViaProxy: feedInfo.FetchViaProxy,
ScraperRules: feedInfo.ScraperRules,
RewriteRules: feedInfo.RewriteRules,
BlocklistRules: feedInfo.BlocklistRules,
KeeplistRules: feedInfo.KeeplistRules,
})
if err != nil { if err != nil {
json.ServerError(w, r, err) json.ServerError(w, r, err)
return return
@ -101,14 +73,14 @@ func (h *handler) refreshAllFeeds(w http.ResponseWriter, r *http.Request) {
} }
func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) { func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
feedID := request.RouteInt64Param(r, "feedID") var feedModificationRequest model.FeedModificationRequest
feedChanges, err := decodeFeedModificationRequest(r.Body) if err := json_parser.NewDecoder(r.Body).Decode(&feedModificationRequest); err != nil {
if err != nil {
json.BadRequest(w, r, err) json.BadRequest(w, r, err)
return return
} }
userID := request.UserID(r) userID := request.UserID(r)
feedID := request.RouteInt64Param(r, "feedID")
originalFeed, err := h.store.FeedByID(userID, feedID) originalFeed, err := h.store.FeedByID(userID, feedID)
if err != nil { if err != nil {
@ -121,13 +93,12 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
return return
} }
feedChanges.Update(originalFeed) if validationErr := validator.ValidateFeedModification(h.store, userID, &feedModificationRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
if !h.store.CategoryIDExists(userID, originalFeed.Category.ID) {
json.BadRequest(w, r, errors.New("This category_id doesn't exists or doesn't belongs to this user"))
return return
} }
feedModificationRequest.Patch(originalFeed)
if err := h.store.UpdateFeed(originalFeed); err != nil { if err := h.store.UpdateFeed(originalFeed); err != nil {
json.ServerError(w, r, err) json.ServerError(w, r, err)
return return

View file

@ -23,150 +23,10 @@ type entriesResponse struct {
Entries model.Entries `json:"entries"` Entries model.Entries `json:"entries"`
} }
type subscriptionDiscoveryRequest struct {
URL string `json:"url"`
UserAgent string `json:"user_agent"`
Username string `json:"username"`
Password string `json:"password"`
FetchViaProxy bool `json:"fetch_via_proxy"`
}
func decodeSubscriptionDiscoveryRequest(r io.ReadCloser) (*subscriptionDiscoveryRequest, error) {
defer r.Close()
var s subscriptionDiscoveryRequest
decoder := json.NewDecoder(r)
if err := decoder.Decode(&s); err != nil {
return nil, fmt.Errorf("invalid JSON payload: %v", err)
}
return &s, nil
}
type feedCreationResponse struct { type feedCreationResponse struct {
FeedID int64 `json:"feed_id"` FeedID int64 `json:"feed_id"`
} }
type feedCreationRequest struct {
FeedURL string `json:"feed_url"`
CategoryID int64 `json:"category_id"`
UserAgent string `json:"user_agent"`
Username string `json:"username"`
Password string `json:"password"`
Crawler bool `json:"crawler"`
Disabled bool `json:"disabled"`
IgnoreHTTPCache bool `json:"ignore_http_cache"`
FetchViaProxy bool `json:"fetch_via_proxy"`
ScraperRules string `json:"scraper_rules"`
RewriteRules string `json:"rewrite_rules"`
BlocklistRules string `json:"blocklist_rules"`
KeeplistRules string `json:"keeplist_rules"`
}
func decodeFeedCreationRequest(r io.ReadCloser) (*feedCreationRequest, error) {
defer r.Close()
var fc feedCreationRequest
decoder := json.NewDecoder(r)
if err := decoder.Decode(&fc); err != nil {
return nil, fmt.Errorf("Invalid JSON payload: %v", err)
}
return &fc, nil
}
type feedModificationRequest struct {
FeedURL *string `json:"feed_url"`
SiteURL *string `json:"site_url"`
Title *string `json:"title"`
ScraperRules *string `json:"scraper_rules"`
RewriteRules *string `json:"rewrite_rules"`
BlocklistRules *string `json:"blocklist_rules"`
KeeplistRules *string `json:"keeplist_rules"`
Crawler *bool `json:"crawler"`
UserAgent *string `json:"user_agent"`
Username *string `json:"username"`
Password *string `json:"password"`
CategoryID *int64 `json:"category_id"`
Disabled *bool `json:"disabled"`
IgnoreHTTPCache *bool `json:"ignore_http_cache"`
FetchViaProxy *bool `json:"fetch_via_proxy"`
}
func (f *feedModificationRequest) Update(feed *model.Feed) {
if f.FeedURL != nil && *f.FeedURL != "" {
feed.FeedURL = *f.FeedURL
}
if f.SiteURL != nil && *f.SiteURL != "" {
feed.SiteURL = *f.SiteURL
}
if f.Title != nil && *f.Title != "" {
feed.Title = *f.Title
}
if f.ScraperRules != nil {
feed.ScraperRules = *f.ScraperRules
}
if f.RewriteRules != nil {
feed.RewriteRules = *f.RewriteRules
}
if f.KeeplistRules != nil {
feed.KeeplistRules = *f.KeeplistRules
}
if f.BlocklistRules != nil {
feed.BlocklistRules = *f.BlocklistRules
}
if f.Crawler != nil {
feed.Crawler = *f.Crawler
}
if f.UserAgent != nil {
feed.UserAgent = *f.UserAgent
}
if f.Username != nil {
feed.Username = *f.Username
}
if f.Password != nil {
feed.Password = *f.Password
}
if f.CategoryID != nil && *f.CategoryID > 0 {
feed.Category.ID = *f.CategoryID
}
if f.Disabled != nil {
feed.Disabled = *f.Disabled
}
if f.IgnoreHTTPCache != nil {
feed.IgnoreHTTPCache = *f.IgnoreHTTPCache
}
if f.FetchViaProxy != nil {
feed.FetchViaProxy = *f.FetchViaProxy
}
}
func decodeFeedModificationRequest(r io.ReadCloser) (*feedModificationRequest, error) {
defer r.Close()
var feed feedModificationRequest
decoder := json.NewDecoder(r)
if err := decoder.Decode(&feed); err != nil {
return nil, fmt.Errorf("Unable to decode feed modification JSON object: %v", err)
}
return &feed, nil
}
func decodeEntryStatusRequest(r io.ReadCloser) ([]int64, string, error) { func decodeEntryStatusRequest(r io.ReadCloser) ([]int64, string, error) {
type payload struct { type payload struct {
EntryIDs []int64 `json:"entry_ids"` EntryIDs []int64 `json:"entry_ids"`

View file

@ -1,220 +0,0 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api // import "miniflux.app/api"
import (
"testing"
"miniflux.app/model"
)
func TestUpdateFeedURL(t *testing.T) {
feedURL := "http://example.com/"
changes := &feedModificationRequest{FeedURL: &feedURL}
feed := &model.Feed{FeedURL: "http://example.org/"}
changes.Update(feed)
if feed.FeedURL != feedURL {
t.Errorf(`Unexpected value, got %q instead of %q`, feed.FeedURL, feedURL)
}
}
func TestUpdateFeedURLWithEmptyString(t *testing.T) {
feedURL := ""
changes := &feedModificationRequest{FeedURL: &feedURL}
feed := &model.Feed{FeedURL: "http://example.org/"}
changes.Update(feed)
if feed.FeedURL == feedURL {
t.Error(`The FeedURL should not be modified`)
}
}
func TestUpdateFeedURLWhenNotSet(t *testing.T) {
changes := &feedModificationRequest{}
feed := &model.Feed{FeedURL: "http://example.org/"}
changes.Update(feed)
if feed.FeedURL != "http://example.org/" {
t.Error(`The FeedURL should not be modified`)
}
}
func TestUpdateFeedSiteURL(t *testing.T) {
siteURL := "http://example.com/"
changes := &feedModificationRequest{SiteURL: &siteURL}
feed := &model.Feed{SiteURL: "http://example.org/"}
changes.Update(feed)
if feed.SiteURL != siteURL {
t.Errorf(`Unexpected value, got %q instead of %q`, feed.SiteURL, siteURL)
}
}
func TestUpdateFeedSiteURLWithEmptyString(t *testing.T) {
siteURL := ""
changes := &feedModificationRequest{FeedURL: &siteURL}
feed := &model.Feed{SiteURL: "http://example.org/"}
changes.Update(feed)
if feed.SiteURL == siteURL {
t.Error(`The FeedURL should not be modified`)
}
}
func TestUpdateFeedSiteURLWhenNotSet(t *testing.T) {
changes := &feedModificationRequest{}
feed := &model.Feed{SiteURL: "http://example.org/"}
changes.Update(feed)
if feed.SiteURL != "http://example.org/" {
t.Error(`The SiteURL should not be modified`)
}
}
func TestUpdateFeedTitle(t *testing.T) {
title := "Example 2"
changes := &feedModificationRequest{Title: &title}
feed := &model.Feed{Title: "Example"}
changes.Update(feed)
if feed.Title != title {
t.Errorf(`Unexpected value, got %q instead of %q`, feed.Title, title)
}
}
func TestUpdateFeedTitleWithEmptyString(t *testing.T) {
title := ""
changes := &feedModificationRequest{Title: &title}
feed := &model.Feed{Title: "Example"}
changes.Update(feed)
if feed.Title == title {
t.Error(`The Title should not be modified`)
}
}
func TestUpdateFeedTitleWhenNotSet(t *testing.T) {
changes := &feedModificationRequest{}
feed := &model.Feed{Title: "Example"}
changes.Update(feed)
if feed.Title != "Example" {
t.Error(`The Title should not be modified`)
}
}
func TestUpdateFeedUsername(t *testing.T) {
username := "Alice"
changes := &feedModificationRequest{Username: &username}
feed := &model.Feed{Username: "Bob"}
changes.Update(feed)
if feed.Username != username {
t.Errorf(`Unexpected value, got %q instead of %q`, feed.Username, username)
}
}
func TestUpdateFeedUsernameWithEmptyString(t *testing.T) {
username := ""
changes := &feedModificationRequest{Username: &username}
feed := &model.Feed{Username: "Bob"}
changes.Update(feed)
if feed.Username != "" {
t.Error(`The Username should be empty now`)
}
}
func TestUpdateFeedUsernameWhenNotSet(t *testing.T) {
changes := &feedModificationRequest{}
feed := &model.Feed{Username: "Alice"}
changes.Update(feed)
if feed.Username != "Alice" {
t.Error(`The Username should not be modified`)
}
}
func TestUpdateFeedDisabled(t *testing.T) {
valueTrue := true
valueFalse := false
scenarios := []struct {
changes *feedModificationRequest
feed *model.Feed
expected bool
}{
{&feedModificationRequest{}, &model.Feed{Disabled: true}, true},
{&feedModificationRequest{Disabled: &valueTrue}, &model.Feed{Disabled: true}, true},
{&feedModificationRequest{Disabled: &valueFalse}, &model.Feed{Disabled: true}, false},
{&feedModificationRequest{}, &model.Feed{Disabled: false}, false},
{&feedModificationRequest{Disabled: &valueTrue}, &model.Feed{Disabled: false}, true},
{&feedModificationRequest{Disabled: &valueFalse}, &model.Feed{Disabled: false}, false},
}
for _, scenario := range scenarios {
scenario.changes.Update(scenario.feed)
if scenario.feed.Disabled != scenario.expected {
t.Errorf(`Unexpected result, got %v, want: %v`,
scenario.feed.Disabled,
scenario.expected,
)
}
}
}
func TestUpdateFeedCategory(t *testing.T) {
categoryID := int64(1)
changes := &feedModificationRequest{CategoryID: &categoryID}
feed := &model.Feed{Category: &model.Category{ID: 42}}
changes.Update(feed)
if feed.Category.ID != categoryID {
t.Errorf(`Unexpected value, got %q instead of %q`, feed.Username, categoryID)
}
}
func TestUpdateFeedCategoryWithZero(t *testing.T) {
categoryID := int64(0)
changes := &feedModificationRequest{CategoryID: &categoryID}
feed := &model.Feed{Category: &model.Category{ID: 42}}
changes.Update(feed)
if feed.Category.ID != 42 {
t.Error(`The CategoryID should not be modified`)
}
}
func TestUpdateFeedCategoryWhenNotSet(t *testing.T) {
changes := &feedModificationRequest{}
feed := &model.Feed{Category: &model.Category{ID: 42}}
changes.Update(feed)
if feed.Category.ID != 42 {
t.Error(`The CategoryID should not be modified`)
}
}
func TestUpdateFeedToIgnoreCache(t *testing.T) {
value := true
changes := &feedModificationRequest{IgnoreHTTPCache: &value}
feed := &model.Feed{IgnoreHTTPCache: false}
changes.Update(feed)
if feed.IgnoreHTTPCache != value {
t.Errorf(`The field IgnoreHTTPCache should be %v`, value)
}
}
func TestUpdateFeedToFetchViaProxy(t *testing.T) {
value := true
changes := &feedModificationRequest{FetchViaProxy: &value}
feed := &model.Feed{FetchViaProxy: false}
changes.Update(feed)
if feed.FetchViaProxy != value {
t.Errorf(`The field FetchViaProxy should be %v`, value)
}
}

View file

@ -5,25 +5,33 @@
package api // import "miniflux.app/api" package api // import "miniflux.app/api"
import ( import (
json_parser "encoding/json"
"net/http" "net/http"
"miniflux.app/http/response/json" "miniflux.app/http/response/json"
"miniflux.app/model"
"miniflux.app/reader/subscription" "miniflux.app/reader/subscription"
"miniflux.app/validator"
) )
func (h *handler) getSubscriptions(w http.ResponseWriter, r *http.Request) { func (h *handler) discoverSubscriptions(w http.ResponseWriter, r *http.Request) {
subscriptionRequest, bodyErr := decodeSubscriptionDiscoveryRequest(r.Body) var subscriptionDiscoveryRequest model.SubscriptionDiscoveryRequest
if bodyErr != nil { if err := json_parser.NewDecoder(r.Body).Decode(&subscriptionDiscoveryRequest); err != nil {
json.BadRequest(w, r, bodyErr) json.BadRequest(w, r, err)
return
}
if validationErr := validator.ValidateSubscriptionDiscovery(&subscriptionDiscoveryRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return return
} }
subscriptions, finderErr := subscription.FindSubscriptions( subscriptions, finderErr := subscription.FindSubscriptions(
subscriptionRequest.URL, subscriptionDiscoveryRequest.URL,
subscriptionRequest.UserAgent, subscriptionDiscoveryRequest.UserAgent,
subscriptionRequest.Username, subscriptionDiscoveryRequest.Username,
subscriptionRequest.Password, subscriptionDiscoveryRequest.Password,
subscriptionRequest.FetchViaProxy, subscriptionDiscoveryRequest.FetchViaProxy,
) )
if finderErr != nil { if finderErr != nil {
json.ServerError(w, r, finderErr) json.ServerError(w, r, finderErr)

View file

@ -314,8 +314,7 @@ func (c *Client) UpdateFeed(feedID int64, feedChanges *FeedModificationRequest)
defer body.Close() defer body.Close()
var f *Feed var f *Feed
decoder := json.NewDecoder(body) if err := json.NewDecoder(body).Decode(&f); err != nil {
if err := decoder.Decode(&f); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err) return nil, fmt.Errorf("miniflux: response error (%v)", err)
} }

View file

@ -241,6 +241,13 @@ var translations = map[string]string{
"error.settings_mandatory_fields": "Die Felder für Benutzername, Thema, Sprache und Zeitzone sind obligatorisch.", "error.settings_mandatory_fields": "Die Felder für Benutzername, Thema, Sprache und Zeitzone sind obligatorisch.",
"error.entries_per_page_invalid": "Die Anzahl der Einträge pro Seite ist ungültig.", "error.entries_per_page_invalid": "Die Anzahl der Einträge pro Seite ist ungültig.",
"error.feed_mandatory_fields": "Die URL und die Kategorie sind obligatorisch.", "error.feed_mandatory_fields": "Die URL und die Kategorie sind obligatorisch.",
"error.feed_already_exists": "Dieser Feed existiert bereits.",
"error.invalid_feed_url": "Ungültige Feed-URL.",
"error.invalid_site_url": "Ungültige Site-URL.",
"error.feed_url_not_empty": "Die Feed-URL darf nicht leer sein.",
"error.site_url_not_empty": "Die Site-URL darf nicht leer sein.",
"error.feed_title_not_empty": "Der Feed-Titel darf nicht leer sein.",
"error.feed_category_not_found": "Diese Kategorie existiert nicht oder gehört nicht zu diesem Benutzer.",
"error.user_mandatory_fields": "Der Benutzername ist obligatorisch.", "error.user_mandatory_fields": "Der Benutzername ist obligatorisch.",
"error.api_key_already_exists": "Dieser API-Schlüssel ist bereits vorhanden.", "error.api_key_already_exists": "Dieser API-Schlüssel ist bereits vorhanden.",
"error.unable_to_create_api_key": "Dieser API-Schlüssel kann nicht erstellt werden.", "error.unable_to_create_api_key": "Dieser API-Schlüssel kann nicht erstellt werden.",
@ -599,6 +606,13 @@ var translations = map[string]string{
"error.settings_mandatory_fields": "The username, theme, language and timezone fields are mandatory.", "error.settings_mandatory_fields": "The username, theme, language and timezone fields are mandatory.",
"error.entries_per_page_invalid": "The number of entries per page is not valid.", "error.entries_per_page_invalid": "The number of entries per page is not valid.",
"error.feed_mandatory_fields": "The URL and the category are mandatory.", "error.feed_mandatory_fields": "The URL and the category are mandatory.",
"error.feed_already_exists": "This feed already exists.",
"error.invalid_feed_url": "Invalid feed URL.",
"error.invalid_site_url": "Invalid site URL.",
"error.feed_url_not_empty": "The feed URL cannot be empty.",
"error.site_url_not_empty": "The site URL cannot be empty.",
"error.feed_title_not_empty": "The feed title cannot be empty.",
"error.feed_category_not_found": "This category does not exist or does not belong to this user.",
"error.user_mandatory_fields": "The username is mandatory.", "error.user_mandatory_fields": "The username is mandatory.",
"error.api_key_already_exists": "This API Key already exists.", "error.api_key_already_exists": "This API Key already exists.",
"error.unable_to_create_api_key": "Unable to create this API Key.", "error.unable_to_create_api_key": "Unable to create this API Key.",
@ -929,6 +943,13 @@ var translations = map[string]string{
"error.settings_mandatory_fields": "Los campos de nombre de usuario, tema, idioma y zona horaria son obligatorios.", "error.settings_mandatory_fields": "Los campos de nombre de usuario, tema, idioma y zona horaria son obligatorios.",
"error.entries_per_page_invalid": "El número de entradas por página no es válido.", "error.entries_per_page_invalid": "El número de entradas por página no es válido.",
"error.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.", "error.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.",
"error.feed_already_exists": "Este feed ya existe.",
"error.invalid_feed_url": "URL de feed no válida.",
"error.invalid_site_url": "URL del sitio no válida.",
"error.feed_url_not_empty": "La URL del feed no puede estar vacía.",
"error.site_url_not_empty": "La URL del sitio no puede estar vacía.",
"error.feed_title_not_empty": "El título del feed no puede estar vacío.",
"error.feed_category_not_found": "Esta categoría no existe o no pertenece a este usuario.",
"error.user_mandatory_fields": "El nombre de usuario es obligatorio.", "error.user_mandatory_fields": "El nombre de usuario es obligatorio.",
"error.api_key_already_exists": "Esta clave API ya existe.", "error.api_key_already_exists": "Esta clave API ya existe.",
"error.unable_to_create_api_key": "No se puede crear esta clave API.", "error.unable_to_create_api_key": "No se puede crear esta clave API.",
@ -1263,6 +1284,13 @@ var translations = map[string]string{
"error.settings_mandatory_fields": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.", "error.settings_mandatory_fields": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.",
"error.entries_per_page_invalid": "Le nombre d'entrées par page n'est pas valide.", "error.entries_per_page_invalid": "Le nombre d'entrées par page n'est pas valide.",
"error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.", "error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.",
"error.feed_already_exists": "Ce flux existe déjà.",
"error.invalid_feed_url": "URL de flux non valide.",
"error.invalid_site_url": "URL de site non valide.",
"error.feed_url_not_empty": "L'URL du flux ne peut pas être vide.",
"error.site_url_not_empty": "L'URL du site ne peut pas être vide.",
"error.feed_title_not_empty": "Le titre du flux ne peut pas être vide.",
"error.feed_category_not_found": "Cette catégorie n'existe pas ou n'appartient pas à cet utilisateur.",
"error.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.", "error.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.",
"error.api_key_already_exists": "Cette clé d'API existe déjà.", "error.api_key_already_exists": "Cette clé d'API existe déjà.",
"error.unable_to_create_api_key": "Impossible de créer cette clé d'API.", "error.unable_to_create_api_key": "Impossible de créer cette clé d'API.",
@ -1617,6 +1645,13 @@ var translations = map[string]string{
"error.settings_mandatory_fields": "Il nome utente, il tema, la lingua ed il fuso orario sono campi obbligatori.", "error.settings_mandatory_fields": "Il nome utente, il tema, la lingua ed il fuso orario sono campi obbligatori.",
"error.entries_per_page_invalid": "Il numero di articoli per pagina non è valido.", "error.entries_per_page_invalid": "Il numero di articoli per pagina non è valido.",
"error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.", "error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.",
"error.feed_already_exists": "Questo feed esiste già.",
"error.invalid_feed_url": "URL del feed non valido.",
"error.invalid_site_url": "URL del sito non valido.",
"error.feed_url_not_empty": "L'URL del feed non può essere vuoto.",
"error.site_url_not_empty": "L'URL del sito non può essere vuoto.",
"error.feed_title_not_empty": "Il titolo del feed non può essere vuoto.",
"error.feed_category_not_found": "Questa categoria non esiste o non appartiene a questo utente.",
"error.user_mandatory_fields": "Il nome utente è obbligatorio.", "error.user_mandatory_fields": "Il nome utente è obbligatorio.",
"error.api_key_already_exists": "Questa chiave API esiste già.", "error.api_key_already_exists": "Questa chiave API esiste già.",
"error.unable_to_create_api_key": "Impossibile creare questa chiave API.", "error.unable_to_create_api_key": "Impossibile creare questa chiave API.",
@ -1951,6 +1986,13 @@ var translations = map[string]string{
"error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンの全てが必要です。", "error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンの全てが必要です。",
"error.entries_per_page_invalid": "ページあたりのエントリ数が無効です。", "error.entries_per_page_invalid": "ページあたりのエントリ数が無効です。",
"error.feed_mandatory_fields": "URL と カテゴリが必要です。", "error.feed_mandatory_fields": "URL と カテゴリが必要です。",
"error.feed_already_exists": "このフィードはすでに存在します。",
"error.invalid_feed_url": "無効なフィードURL。",
"error.invalid_site_url": "無効なサイトURL。",
"error.feed_url_not_empty": "フィードURLを空にすることはできません。",
"error.site_url_not_empty": "サイトのURLを空にすることはできません。",
"error.feed_title_not_empty": "フィードのタイトルを空にすることはできません。",
"error.feed_category_not_found": "このカテゴリは存在しないか、このユーザーに属していません。",
"error.user_mandatory_fields": "ユーザー名が必要です。", "error.user_mandatory_fields": "ユーザー名が必要です。",
"error.api_key_already_exists": "このAPIキーは既に存在します。", "error.api_key_already_exists": "このAPIキーは既に存在します。",
"error.unable_to_create_api_key": "このAPIキーを作成できません。", "error.unable_to_create_api_key": "このAPIキーを作成できません。",
@ -2285,6 +2327,13 @@ var translations = map[string]string{
"error.settings_mandatory_fields": "Gebruikersnaam, skin, taal en tijdzone zijn verplicht.", "error.settings_mandatory_fields": "Gebruikersnaam, skin, taal en tijdzone zijn verplicht.",
"error.entries_per_page_invalid": "Het aantal inzendingen per pagina is niet geldig.", "error.entries_per_page_invalid": "Het aantal inzendingen per pagina is niet geldig.",
"error.feed_mandatory_fields": "The URL en de categorie zijn verplicht.", "error.feed_mandatory_fields": "The URL en de categorie zijn verplicht.",
"error.feed_already_exists": "Deze feed bestaat al.",
"error.invalid_feed_url": "Ongeldige feed-URL.",
"error.invalid_site_url": "Ongeldige site-URL.",
"error.feed_url_not_empty": "De feed-URL mag niet leeg zijn.",
"error.site_url_not_empty": "De site-URL mag niet leeg zijn.",
"error.feed_title_not_empty": "De feedtitel mag niet leeg zijn.",
"error.feed_category_not_found": "Deze categorie bestaat niet of behoort niet tot deze gebruiker.",
"error.user_mandatory_fields": "Gebruikersnaam is verplicht", "error.user_mandatory_fields": "Gebruikersnaam is verplicht",
"error.api_key_already_exists": "This API Key already exists.", "error.api_key_already_exists": "This API Key already exists.",
"error.unable_to_create_api_key": "Kan deze API-sleutel niet maken.", "error.unable_to_create_api_key": "Kan deze API-sleutel niet maken.",
@ -2639,6 +2688,13 @@ var translations = map[string]string{
"error.settings_mandatory_fields": "Pola nazwy użytkownika, tematu, języka i strefy czasowej są obowiązkowe.", "error.settings_mandatory_fields": "Pola nazwy użytkownika, tematu, języka i strefy czasowej są obowiązkowe.",
"error.entries_per_page_invalid": "Liczba wpisów na stronę jest nieprawidłowa.", "error.entries_per_page_invalid": "Liczba wpisów na stronę jest nieprawidłowa.",
"error.feed_mandatory_fields": "URL i kategoria są obowiązkowe.", "error.feed_mandatory_fields": "URL i kategoria są obowiązkowe.",
"error.feed_already_exists": "Ten kanał już istnieje.",
"error.invalid_feed_url": "Nieprawidłowy adres URL kanału.",
"error.invalid_site_url": "Nieprawidłowy adres URL witryny.",
"error.feed_url_not_empty": "Adres URL kanału nie może być pusty.",
"error.site_url_not_empty": "Adres URL witryny nie może być pusty.",
"error.feed_title_not_empty": "Tytuł kanału nie może być pusty.",
"error.feed_category_not_found": "Ta kategoria nie istnieje lub nie należy do tego użytkownika.",
"error.user_mandatory_fields": "Nazwa użytkownika jest obowiązkowa.", "error.user_mandatory_fields": "Nazwa użytkownika jest obowiązkowa.",
"error.api_key_already_exists": "Deze API-sleutel bestaat al.", "error.api_key_already_exists": "Deze API-sleutel bestaat al.",
"error.unable_to_create_api_key": "Nie można utworzyć tego klucza API.", "error.unable_to_create_api_key": "Nie można utworzyć tego klucza API.",
@ -2997,6 +3053,13 @@ var translations = map[string]string{
"error.settings_mandatory_fields": "Os campos de nome de usuário, tema, idioma e fuso horário são obrigatórios.", "error.settings_mandatory_fields": "Os campos de nome de usuário, tema, idioma e fuso horário são obrigatórios.",
"error.entries_per_page_invalid": "O número de itens por página é inválido.", "error.entries_per_page_invalid": "O número de itens por página é inválido.",
"error.feed_mandatory_fields": "O campo de URL e categoria são obrigatórios.", "error.feed_mandatory_fields": "O campo de URL e categoria são obrigatórios.",
"error.feed_already_exists": "Este feed já existe.",
"error.invalid_feed_url": "URL de feed inválido.",
"error.invalid_site_url": "URL de site inválido.",
"error.feed_url_not_empty": "O URL do feed não pode estar vazio.",
"error.site_url_not_empty": "O URL do site não pode estar vazio.",
"error.feed_title_not_empty": "O título do feed não pode estar vazio.",
"error.feed_category_not_found": "Esta categoria não existe ou não pertence a este usuário.",
"error.user_mandatory_fields": "O nome de usuário é obrigatório.", "error.user_mandatory_fields": "O nome de usuário é obrigatório.",
"error.api_key_already_exists": "Essa chave de API já existe.", "error.api_key_already_exists": "Essa chave de API já existe.",
"error.unable_to_create_api_key": "Não foi possível criar uma chave de API.", "error.unable_to_create_api_key": "Não foi possível criar uma chave de API.",
@ -3333,6 +3396,13 @@ var translations = map[string]string{
"error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.", "error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.",
"error.entries_per_page_invalid": "Количество записей на странице недействительно.", "error.entries_per_page_invalid": "Количество записей на странице недействительно.",
"error.feed_mandatory_fields": "URL и категория обязательны.", "error.feed_mandatory_fields": "URL и категория обязательны.",
"error.feed_already_exists": "Этот фид уже существует.",
"error.invalid_feed_url": "Недействительный URL фида.",
"error.invalid_site_url": "Недействительный URL сайта.",
"error.feed_url_not_empty": "URL-адрес канала не может быть пустым.",
"error.site_url_not_empty": "URL сайта не может быть пустым.",
"error.feed_title_not_empty": "Заголовок фида не может быть пустым.",
"error.feed_category_not_found": "Эта категория не существует или не принадлежит этому пользователю.",
"error.user_mandatory_fields": "Имя пользователя обязательно.", "error.user_mandatory_fields": "Имя пользователя обязательно.",
"error.api_key_already_exists": "Этот ключ API уже существует.", "error.api_key_already_exists": "Этот ключ API уже существует.",
"error.unable_to_create_api_key": "Невозможно создать этот ключ API.", "error.unable_to_create_api_key": "Невозможно создать этот ключ API.",
@ -3671,6 +3741,13 @@ var translations = map[string]string{
"error.settings_mandatory_fields": "必须填写用户名、主题、语言以及时区", "error.settings_mandatory_fields": "必须填写用户名、主题、语言以及时区",
"error.entries_per_page_invalid": "每页的条目数无效。", "error.entries_per_page_invalid": "每页的条目数无效。",
"error.feed_mandatory_fields": "必须填写 URL 和分类", "error.feed_mandatory_fields": "必须填写 URL 和分类",
"error.feed_already_exists": "此供稿已存在。",
"error.invalid_feed_url": "供稿网址无效。",
"error.invalid_site_url": "无效的网站网址。",
"error.feed_url_not_empty": "供稿网址不能为空。",
"error.site_url_not_empty": "网站网址不能为空。",
"error.feed_title_not_empty": "供稿标题不能为空。",
"error.feed_category_not_found": "此类别不存在或不属于该用户。",
"error.user_mandatory_fields": "必须填写用户名", "error.user_mandatory_fields": "必须填写用户名",
"error.api_key_already_exists": "此API密钥已存在。", "error.api_key_already_exists": "此API密钥已存在。",
"error.unable_to_create_api_key": "无法创建此API密钥。", "error.unable_to_create_api_key": "无法创建此API密钥。",
@ -3783,15 +3860,15 @@ var translations = map[string]string{
} }
var translationsChecksums = map[string]string{ var translationsChecksums = map[string]string{
"de_DE": "c8d6021599cfda4f853bd5ec1e1b065f03633ada9211ee22879ea778ba464572", "de_DE": "9db01e4337375c37edae008d93ccf8f1ab50f6b30a468cefe75257e5b1364513",
"en_US": "781a7a6b54f439d76fe56fca7cb07412a04e71edebf53563f5cca27a0cd2533a", "en_US": "506dc83a66e38147328f924b15f1280a71fb3cc5f83c1f690c6029d137f8baee",
"es_ES": "4d602461f5ed9c4aaf59e8828d2b09d0cc45d06ba77d89ba0ef9662b580aebc0", "es_ES": "37c7d271dcae76f524a8e52b86bfaa9dc680954ba75ed53e7945b95ffe2ae9e9",
"fr_FR": "3a0a008d0857fa5eb8a018ce5e348d7ccabe08a67849c72c6e7611e6b5b49aa7", "fr_FR": "ed626b9752239c0f89a17ff28c5003a5ab930a3f0d1df5628b23e8de3587c0f5",
"it_IT": "7222e3610ad3741aa7aff957f70524b63ffe3f6729198899231765335861a108", "it_IT": "015892b01bc85407a0813eb60deb1a1bbbfaf2b72bb9194e13378083f6f54b84",
"ja_JP": "f0ab6dd77c78717d25d88baad39c487c913720be4b3473a3f0aa3aa538318deb", "ja_JP": "ea1843af4638ce58cfe4ca730133e7ef178c6242b6bd253c714b179b45efde9f",
"nl_NL": "1e0872b89fb78a6de2ae989d054963226146c3eeff4b2883cf2bf8df96c13846", "nl_NL": "fe3d9e519d3326d0ff51590011ac6cb344e26e0aa241a8295fb38ca7a7c2191c",
"pl_PL": "2513808a13925549c9ba27c52a20916d18a5222dd8ba6a14520798766889b076", "pl_PL": "5af4497ab4420ff8cec45b86dc65ddc685cd9cca0fb750e238d57f1a8d43c32f",
"pt_BR": "b5fc4d9e0dedc554579154f2fff772b108baf317c9a952d688db0df260674b3b", "pt_BR": "052cfe35211165ed7de9e99109a819d362b5a69b490bb58cc6d884e8fbbf4469",
"ru_RU": "d9bedead0757deae57da909c7d5297853c2186acb8ebf7cf91d0eef7c1a17d19", "ru_RU": "c7216ede62f1c1d18b2ad05bb20a2dfdab04e7bb968773a705da8c26cd5bdcd8",
"zh_CN": "2526d0139ca0a2004f2db0864cbc9c3da55c3c7f45e1a244fea3c39d5d39e0f9", "zh_CN": "8e02550d068e3d8020bd7923a00e5a045dd09db1cc0dfdaa2294417175068743",
} }

View file

@ -236,6 +236,13 @@
"error.settings_mandatory_fields": "Die Felder für Benutzername, Thema, Sprache und Zeitzone sind obligatorisch.", "error.settings_mandatory_fields": "Die Felder für Benutzername, Thema, Sprache und Zeitzone sind obligatorisch.",
"error.entries_per_page_invalid": "Die Anzahl der Einträge pro Seite ist ungültig.", "error.entries_per_page_invalid": "Die Anzahl der Einträge pro Seite ist ungültig.",
"error.feed_mandatory_fields": "Die URL und die Kategorie sind obligatorisch.", "error.feed_mandatory_fields": "Die URL und die Kategorie sind obligatorisch.",
"error.feed_already_exists": "Dieser Feed existiert bereits.",
"error.invalid_feed_url": "Ungültige Feed-URL.",
"error.invalid_site_url": "Ungültige Site-URL.",
"error.feed_url_not_empty": "Die Feed-URL darf nicht leer sein.",
"error.site_url_not_empty": "Die Site-URL darf nicht leer sein.",
"error.feed_title_not_empty": "Der Feed-Titel darf nicht leer sein.",
"error.feed_category_not_found": "Diese Kategorie existiert nicht oder gehört nicht zu diesem Benutzer.",
"error.user_mandatory_fields": "Der Benutzername ist obligatorisch.", "error.user_mandatory_fields": "Der Benutzername ist obligatorisch.",
"error.api_key_already_exists": "Dieser API-Schlüssel ist bereits vorhanden.", "error.api_key_already_exists": "Dieser API-Schlüssel ist bereits vorhanden.",
"error.unable_to_create_api_key": "Dieser API-Schlüssel kann nicht erstellt werden.", "error.unable_to_create_api_key": "Dieser API-Schlüssel kann nicht erstellt werden.",

View file

@ -240,6 +240,13 @@
"error.settings_mandatory_fields": "The username, theme, language and timezone fields are mandatory.", "error.settings_mandatory_fields": "The username, theme, language and timezone fields are mandatory.",
"error.entries_per_page_invalid": "The number of entries per page is not valid.", "error.entries_per_page_invalid": "The number of entries per page is not valid.",
"error.feed_mandatory_fields": "The URL and the category are mandatory.", "error.feed_mandatory_fields": "The URL and the category are mandatory.",
"error.feed_already_exists": "This feed already exists.",
"error.invalid_feed_url": "Invalid feed URL.",
"error.invalid_site_url": "Invalid site URL.",
"error.feed_url_not_empty": "The feed URL cannot be empty.",
"error.site_url_not_empty": "The site URL cannot be empty.",
"error.feed_title_not_empty": "The feed title cannot be empty.",
"error.feed_category_not_found": "This category does not exist or does not belong to this user.",
"error.user_mandatory_fields": "The username is mandatory.", "error.user_mandatory_fields": "The username is mandatory.",
"error.api_key_already_exists": "This API Key already exists.", "error.api_key_already_exists": "This API Key already exists.",
"error.unable_to_create_api_key": "Unable to create this API Key.", "error.unable_to_create_api_key": "Unable to create this API Key.",

View file

@ -236,6 +236,13 @@
"error.settings_mandatory_fields": "Los campos de nombre de usuario, tema, idioma y zona horaria son obligatorios.", "error.settings_mandatory_fields": "Los campos de nombre de usuario, tema, idioma y zona horaria son obligatorios.",
"error.entries_per_page_invalid": "El número de entradas por página no es válido.", "error.entries_per_page_invalid": "El número de entradas por página no es válido.",
"error.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.", "error.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.",
"error.feed_already_exists": "Este feed ya existe.",
"error.invalid_feed_url": "URL de feed no válida.",
"error.invalid_site_url": "URL del sitio no válida.",
"error.feed_url_not_empty": "La URL del feed no puede estar vacía.",
"error.site_url_not_empty": "La URL del sitio no puede estar vacía.",
"error.feed_title_not_empty": "El título del feed no puede estar vacío.",
"error.feed_category_not_found": "Esta categoría no existe o no pertenece a este usuario.",
"error.user_mandatory_fields": "El nombre de usuario es obligatorio.", "error.user_mandatory_fields": "El nombre de usuario es obligatorio.",
"error.api_key_already_exists": "Esta clave API ya existe.", "error.api_key_already_exists": "Esta clave API ya existe.",
"error.unable_to_create_api_key": "No se puede crear esta clave API.", "error.unable_to_create_api_key": "No se puede crear esta clave API.",

View file

@ -236,6 +236,13 @@
"error.settings_mandatory_fields": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.", "error.settings_mandatory_fields": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.",
"error.entries_per_page_invalid": "Le nombre d'entrées par page n'est pas valide.", "error.entries_per_page_invalid": "Le nombre d'entrées par page n'est pas valide.",
"error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.", "error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.",
"error.feed_already_exists": "Ce flux existe déjà.",
"error.invalid_feed_url": "URL de flux non valide.",
"error.invalid_site_url": "URL de site non valide.",
"error.feed_url_not_empty": "L'URL du flux ne peut pas être vide.",
"error.site_url_not_empty": "L'URL du site ne peut pas être vide.",
"error.feed_title_not_empty": "Le titre du flux ne peut pas être vide.",
"error.feed_category_not_found": "Cette catégorie n'existe pas ou n'appartient pas à cet utilisateur.",
"error.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.", "error.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.",
"error.api_key_already_exists": "Cette clé d'API existe déjà.", "error.api_key_already_exists": "Cette clé d'API existe déjà.",
"error.unable_to_create_api_key": "Impossible de créer cette clé d'API.", "error.unable_to_create_api_key": "Impossible de créer cette clé d'API.",

View file

@ -236,6 +236,13 @@
"error.settings_mandatory_fields": "Il nome utente, il tema, la lingua ed il fuso orario sono campi obbligatori.", "error.settings_mandatory_fields": "Il nome utente, il tema, la lingua ed il fuso orario sono campi obbligatori.",
"error.entries_per_page_invalid": "Il numero di articoli per pagina non è valido.", "error.entries_per_page_invalid": "Il numero di articoli per pagina non è valido.",
"error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.", "error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.",
"error.feed_already_exists": "Questo feed esiste già.",
"error.invalid_feed_url": "URL del feed non valido.",
"error.invalid_site_url": "URL del sito non valido.",
"error.feed_url_not_empty": "L'URL del feed non può essere vuoto.",
"error.site_url_not_empty": "L'URL del sito non può essere vuoto.",
"error.feed_title_not_empty": "Il titolo del feed non può essere vuoto.",
"error.feed_category_not_found": "Questa categoria non esiste o non appartiene a questo utente.",
"error.user_mandatory_fields": "Il nome utente è obbligatorio.", "error.user_mandatory_fields": "Il nome utente è obbligatorio.",
"error.api_key_already_exists": "Questa chiave API esiste già.", "error.api_key_already_exists": "Questa chiave API esiste già.",
"error.unable_to_create_api_key": "Impossibile creare questa chiave API.", "error.unable_to_create_api_key": "Impossibile creare questa chiave API.",

View file

@ -236,6 +236,13 @@
"error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンの全てが必要です。", "error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンの全てが必要です。",
"error.entries_per_page_invalid": "ページあたりのエントリ数が無効です。", "error.entries_per_page_invalid": "ページあたりのエントリ数が無効です。",
"error.feed_mandatory_fields": "URL と カテゴリが必要です。", "error.feed_mandatory_fields": "URL と カテゴリが必要です。",
"error.feed_already_exists": "このフィードはすでに存在します。",
"error.invalid_feed_url": "無効なフィードURL。",
"error.invalid_site_url": "無効なサイトURL。",
"error.feed_url_not_empty": "フィードURLを空にすることはできません。",
"error.site_url_not_empty": "サイトのURLを空にすることはできません。",
"error.feed_title_not_empty": "フィードのタイトルを空にすることはできません。",
"error.feed_category_not_found": "このカテゴリは存在しないか、このユーザーに属していません。",
"error.user_mandatory_fields": "ユーザー名が必要です。", "error.user_mandatory_fields": "ユーザー名が必要です。",
"error.api_key_already_exists": "このAPIキーは既に存在します。", "error.api_key_already_exists": "このAPIキーは既に存在します。",
"error.unable_to_create_api_key": "このAPIキーを作成できません。", "error.unable_to_create_api_key": "このAPIキーを作成できません。",

View file

@ -236,6 +236,13 @@
"error.settings_mandatory_fields": "Gebruikersnaam, skin, taal en tijdzone zijn verplicht.", "error.settings_mandatory_fields": "Gebruikersnaam, skin, taal en tijdzone zijn verplicht.",
"error.entries_per_page_invalid": "Het aantal inzendingen per pagina is niet geldig.", "error.entries_per_page_invalid": "Het aantal inzendingen per pagina is niet geldig.",
"error.feed_mandatory_fields": "The URL en de categorie zijn verplicht.", "error.feed_mandatory_fields": "The URL en de categorie zijn verplicht.",
"error.feed_already_exists": "Deze feed bestaat al.",
"error.invalid_feed_url": "Ongeldige feed-URL.",
"error.invalid_site_url": "Ongeldige site-URL.",
"error.feed_url_not_empty": "De feed-URL mag niet leeg zijn.",
"error.site_url_not_empty": "De site-URL mag niet leeg zijn.",
"error.feed_title_not_empty": "De feedtitel mag niet leeg zijn.",
"error.feed_category_not_found": "Deze categorie bestaat niet of behoort niet tot deze gebruiker.",
"error.user_mandatory_fields": "Gebruikersnaam is verplicht", "error.user_mandatory_fields": "Gebruikersnaam is verplicht",
"error.api_key_already_exists": "This API Key already exists.", "error.api_key_already_exists": "This API Key already exists.",
"error.unable_to_create_api_key": "Kan deze API-sleutel niet maken.", "error.unable_to_create_api_key": "Kan deze API-sleutel niet maken.",

View file

@ -238,6 +238,13 @@
"error.settings_mandatory_fields": "Pola nazwy użytkownika, tematu, języka i strefy czasowej są obowiązkowe.", "error.settings_mandatory_fields": "Pola nazwy użytkownika, tematu, języka i strefy czasowej są obowiązkowe.",
"error.entries_per_page_invalid": "Liczba wpisów na stronę jest nieprawidłowa.", "error.entries_per_page_invalid": "Liczba wpisów na stronę jest nieprawidłowa.",
"error.feed_mandatory_fields": "URL i kategoria są obowiązkowe.", "error.feed_mandatory_fields": "URL i kategoria są obowiązkowe.",
"error.feed_already_exists": "Ten kanał już istnieje.",
"error.invalid_feed_url": "Nieprawidłowy adres URL kanału.",
"error.invalid_site_url": "Nieprawidłowy adres URL witryny.",
"error.feed_url_not_empty": "Adres URL kanału nie może być pusty.",
"error.site_url_not_empty": "Adres URL witryny nie może być pusty.",
"error.feed_title_not_empty": "Tytuł kanału nie może być pusty.",
"error.feed_category_not_found": "Ta kategoria nie istnieje lub nie należy do tego użytkownika.",
"error.user_mandatory_fields": "Nazwa użytkownika jest obowiązkowa.", "error.user_mandatory_fields": "Nazwa użytkownika jest obowiązkowa.",
"error.api_key_already_exists": "Deze API-sleutel bestaat al.", "error.api_key_already_exists": "Deze API-sleutel bestaat al.",
"error.unable_to_create_api_key": "Nie można utworzyć tego klucza API.", "error.unable_to_create_api_key": "Nie można utworzyć tego klucza API.",

View file

@ -236,6 +236,13 @@
"error.settings_mandatory_fields": "Os campos de nome de usuário, tema, idioma e fuso horário são obrigatórios.", "error.settings_mandatory_fields": "Os campos de nome de usuário, tema, idioma e fuso horário são obrigatórios.",
"error.entries_per_page_invalid": "O número de itens por página é inválido.", "error.entries_per_page_invalid": "O número de itens por página é inválido.",
"error.feed_mandatory_fields": "O campo de URL e categoria são obrigatórios.", "error.feed_mandatory_fields": "O campo de URL e categoria são obrigatórios.",
"error.feed_already_exists": "Este feed já existe.",
"error.invalid_feed_url": "URL de feed inválido.",
"error.invalid_site_url": "URL de site inválido.",
"error.feed_url_not_empty": "O URL do feed não pode estar vazio.",
"error.site_url_not_empty": "O URL do site não pode estar vazio.",
"error.feed_title_not_empty": "O título do feed não pode estar vazio.",
"error.feed_category_not_found": "Esta categoria não existe ou não pertence a este usuário.",
"error.user_mandatory_fields": "O nome de usuário é obrigatório.", "error.user_mandatory_fields": "O nome de usuário é obrigatório.",
"error.api_key_already_exists": "Essa chave de API já existe.", "error.api_key_already_exists": "Essa chave de API já existe.",
"error.unable_to_create_api_key": "Não foi possível criar uma chave de API.", "error.unable_to_create_api_key": "Não foi possível criar uma chave de API.",

View file

@ -238,6 +238,13 @@
"error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.", "error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.",
"error.entries_per_page_invalid": "Количество записей на странице недействительно.", "error.entries_per_page_invalid": "Количество записей на странице недействительно.",
"error.feed_mandatory_fields": "URL и категория обязательны.", "error.feed_mandatory_fields": "URL и категория обязательны.",
"error.feed_already_exists": "Этот фид уже существует.",
"error.invalid_feed_url": "Недействительный URL фида.",
"error.invalid_site_url": "Недействительный URL сайта.",
"error.feed_url_not_empty": "URL-адрес канала не может быть пустым.",
"error.site_url_not_empty": "URL сайта не может быть пустым.",
"error.feed_title_not_empty": "Заголовок фида не может быть пустым.",
"error.feed_category_not_found": "Эта категория не существует или не принадлежит этому пользователю.",
"error.user_mandatory_fields": "Имя пользователя обязательно.", "error.user_mandatory_fields": "Имя пользователя обязательно.",
"error.api_key_already_exists": "Этот ключ API уже существует.", "error.api_key_already_exists": "Этот ключ API уже существует.",
"error.unable_to_create_api_key": "Невозможно создать этот ключ API.", "error.unable_to_create_api_key": "Невозможно создать этот ключ API.",

View file

@ -234,6 +234,13 @@
"error.settings_mandatory_fields": "必须填写用户名、主题、语言以及时区", "error.settings_mandatory_fields": "必须填写用户名、主题、语言以及时区",
"error.entries_per_page_invalid": "每页的条目数无效。", "error.entries_per_page_invalid": "每页的条目数无效。",
"error.feed_mandatory_fields": "必须填写 URL 和分类", "error.feed_mandatory_fields": "必须填写 URL 和分类",
"error.feed_already_exists": "此供稿已存在。",
"error.invalid_feed_url": "供稿网址无效。",
"error.invalid_site_url": "无效的网站网址。",
"error.feed_url_not_empty": "供稿网址不能为空。",
"error.site_url_not_empty": "网站网址不能为空。",
"error.feed_title_not_empty": "供稿标题不能为空。",
"error.feed_category_not_found": "此类别不存在或不属于该用户。",
"error.user_mandatory_fields": "必须填写用户名", "error.user_mandatory_fields": "必须填写用户名",
"error.api_key_already_exists": "此API密钥已存在。", "error.api_key_already_exists": "此API密钥已存在。",
"error.unable_to_create_api_key": "无法创建此API密钥。", "error.unable_to_create_api_key": "无法创建此API密钥。",

View file

@ -13,6 +13,12 @@ import (
"miniflux.app/http/client" "miniflux.app/http/client"
) )
// List of supported schedulers.
const (
SchedulerRoundRobin = "round_robin"
SchedulerEntryFrequency = "entry_frequency"
)
// Feed represents a feed in the application. // Feed represents a feed in the application.
type Feed struct { type Feed struct {
ID int64 `json:"id"` ID int64 `json:"id"`
@ -44,12 +50,6 @@ type Feed struct {
ReadCount int `json:"-"` ReadCount int `json:"-"`
} }
// List of supported schedulers.
const (
SchedulerRoundRobin = "round_robin"
SchedulerEntryFrequency = "entry_frequency"
)
func (f *Feed) String() string { func (f *Feed) String() string {
return fmt.Sprintf("ID=%d, UserID=%d, FeedURL=%s, SiteURL=%s, Title=%s, Category={%s}", return fmt.Sprintf("ID=%d, UserID=%d, FeedURL=%s, SiteURL=%s, Title=%s, Category={%s}",
f.ID, f.ID,
@ -112,5 +112,104 @@ func (f *Feed) ScheduleNextCheck(weeklyCount int) {
} }
} }
// FeedCreationRequest represents the request to create a feed.
type FeedCreationRequest struct {
FeedURL string `json:"feed_url"`
CategoryID int64 `json:"category_id"`
UserAgent string `json:"user_agent"`
Username string `json:"username"`
Password string `json:"password"`
Crawler bool `json:"crawler"`
Disabled bool `json:"disabled"`
IgnoreHTTPCache bool `json:"ignore_http_cache"`
FetchViaProxy bool `json:"fetch_via_proxy"`
ScraperRules string `json:"scraper_rules"`
RewriteRules string `json:"rewrite_rules"`
BlocklistRules string `json:"blocklist_rules"`
KeeplistRules string `json:"keeplist_rules"`
}
// FeedModificationRequest represents the request to update a feed.
type FeedModificationRequest struct {
FeedURL *string `json:"feed_url"`
SiteURL *string `json:"site_url"`
Title *string `json:"title"`
ScraperRules *string `json:"scraper_rules"`
RewriteRules *string `json:"rewrite_rules"`
BlocklistRules *string `json:"blocklist_rules"`
KeeplistRules *string `json:"keeplist_rules"`
Crawler *bool `json:"crawler"`
UserAgent *string `json:"user_agent"`
Username *string `json:"username"`
Password *string `json:"password"`
CategoryID *int64 `json:"category_id"`
Disabled *bool `json:"disabled"`
IgnoreHTTPCache *bool `json:"ignore_http_cache"`
FetchViaProxy *bool `json:"fetch_via_proxy"`
}
// Patch updates a feed with modified values.
func (f *FeedModificationRequest) Patch(feed *Feed) {
if f.FeedURL != nil && *f.FeedURL != "" {
feed.FeedURL = *f.FeedURL
}
if f.SiteURL != nil && *f.SiteURL != "" {
feed.SiteURL = *f.SiteURL
}
if f.Title != nil && *f.Title != "" {
feed.Title = *f.Title
}
if f.ScraperRules != nil {
feed.ScraperRules = *f.ScraperRules
}
if f.RewriteRules != nil {
feed.RewriteRules = *f.RewriteRules
}
if f.KeeplistRules != nil {
feed.KeeplistRules = *f.KeeplistRules
}
if f.BlocklistRules != nil {
feed.BlocklistRules = *f.BlocklistRules
}
if f.Crawler != nil {
feed.Crawler = *f.Crawler
}
if f.UserAgent != nil {
feed.UserAgent = *f.UserAgent
}
if f.Username != nil {
feed.Username = *f.Username
}
if f.Password != nil {
feed.Password = *f.Password
}
if f.CategoryID != nil && *f.CategoryID > 0 {
feed.Category.ID = *f.CategoryID
}
if f.Disabled != nil {
feed.Disabled = *f.Disabled
}
if f.IgnoreHTTPCache != nil {
feed.IgnoreHTTPCache = *f.IgnoreHTTPCache
}
if f.FetchViaProxy != nil {
feed.FetchViaProxy = *f.FetchViaProxy
}
}
// Feeds is a list of feed // Feeds is a list of feed
type Feeds []*Feed type Feeds []*Feed

View file

@ -19,3 +19,11 @@ func OptionalInt(value int) *int {
} }
return nil return nil
} }
// OptionalInt64 populates an optional int64 field.
func OptionalInt64(value int64) *int64 {
if value > 0 {
return &value
}
return nil
}

14
model/subscription.go Normal file
View file

@ -0,0 +1,14 @@
// Copyright 2020 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package model // import "miniflux.app/model"
// SubscriptionDiscoveryRequest represents a request to discover subscriptions.
type SubscriptionDiscoveryRequest struct {
URL string `json:"url"`
UserAgent string `json:"user_agent"`
Username string `json:"username"`
Password string `json:"password"`
FetchViaProxy bool `json:"fetch_via_proxy"`
}

View file

@ -28,37 +28,19 @@ var (
errCategoryNotFound = "Category not found for this user" errCategoryNotFound = "Category not found for this user"
) )
// FeedCreationArgs represents the arguments required to create a new feed.
type FeedCreationArgs struct {
UserID int64
CategoryID int64
FeedURL string
UserAgent string
Username string
Password string
Crawler bool
Disabled bool
IgnoreHTTPCache bool
FetchViaProxy bool
ScraperRules string
RewriteRules string
BlocklistRules string
KeeplistRules string
}
// CreateFeed fetch, parse and store a new feed. // CreateFeed fetch, parse and store a new feed.
func CreateFeed(store *storage.Storage, args *FeedCreationArgs) (*model.Feed, error) { func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model.FeedCreationRequest) (*model.Feed, error) {
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[CreateFeed] FeedURL=%s", args.FeedURL)) defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[CreateFeed] FeedURL=%s", feedCreationRequest.FeedURL))
if !store.CategoryIDExists(args.UserID, args.CategoryID) { if !store.CategoryIDExists(userID, feedCreationRequest.CategoryID) {
return nil, errors.NewLocalizedError(errCategoryNotFound) return nil, errors.NewLocalizedError(errCategoryNotFound)
} }
request := client.NewClientWithConfig(args.FeedURL, config.Opts) request := client.NewClientWithConfig(feedCreationRequest.FeedURL, config.Opts)
request.WithCredentials(args.Username, args.Password) request.WithCredentials(feedCreationRequest.Username, feedCreationRequest.Password)
request.WithUserAgent(args.UserAgent) request.WithUserAgent(feedCreationRequest.UserAgent)
if args.FetchViaProxy { if feedCreationRequest.FetchViaProxy {
request.WithProxy() request.WithProxy()
} }
@ -67,7 +49,7 @@ func CreateFeed(store *storage.Storage, args *FeedCreationArgs) (*model.Feed, er
return nil, requestErr return nil, requestErr
} }
if store.FeedURLExists(args.UserID, response.EffectiveURL) { if store.FeedURLExists(userID, response.EffectiveURL) {
return nil, errors.NewLocalizedError(errDuplicate, response.EffectiveURL) return nil, errors.NewLocalizedError(errDuplicate, response.EffectiveURL)
} }
@ -76,19 +58,19 @@ func CreateFeed(store *storage.Storage, args *FeedCreationArgs) (*model.Feed, er
return nil, parseErr return nil, parseErr
} }
subscription.UserID = args.UserID subscription.UserID = userID
subscription.UserAgent = args.UserAgent subscription.UserAgent = feedCreationRequest.UserAgent
subscription.Username = args.Username subscription.Username = feedCreationRequest.Username
subscription.Password = args.Password subscription.Password = feedCreationRequest.Password
subscription.Crawler = args.Crawler subscription.Crawler = feedCreationRequest.Crawler
subscription.Disabled = args.Disabled subscription.Disabled = feedCreationRequest.Disabled
subscription.IgnoreHTTPCache = args.IgnoreHTTPCache subscription.IgnoreHTTPCache = feedCreationRequest.IgnoreHTTPCache
subscription.FetchViaProxy = args.FetchViaProxy subscription.FetchViaProxy = feedCreationRequest.FetchViaProxy
subscription.ScraperRules = args.ScraperRules subscription.ScraperRules = feedCreationRequest.ScraperRules
subscription.RewriteRules = args.RewriteRules subscription.RewriteRules = feedCreationRequest.RewriteRules
subscription.BlocklistRules = args.BlocklistRules subscription.BlocklistRules = feedCreationRequest.BlocklistRules
subscription.KeeplistRules = args.KeeplistRules subscription.KeeplistRules = feedCreationRequest.KeeplistRules
subscription.WithCategoryID(args.CategoryID) subscription.WithCategoryID(feedCreationRequest.CategoryID)
subscription.WithClientResponse(response) subscription.WithClientResponse(response)
subscription.CheckedNow() subscription.CheckedNow()
@ -100,7 +82,7 @@ func CreateFeed(store *storage.Storage, args *FeedCreationArgs) (*model.Feed, er
logger.Debug("[CreateFeed] Feed saved with ID: %d", subscription.ID) logger.Debug("[CreateFeed] Feed saved with ID: %d", subscription.ID)
checkFeedIcon(store, subscription.ID, subscription.SiteURL, args.FetchViaProxy) checkFeedIcon(store, subscription.ID, subscription.SiteURL, feedCreationRequest.FetchViaProxy)
return subscription, nil return subscription, nil
} }

View file

@ -46,6 +46,38 @@ func TestCreateFeedWithInexistingCategory(t *testing.T) {
} }
} }
func TestCreateFeedWithEmptyFeedURL(t *testing.T) {
client := createClient(t)
categories, err := client.Categories()
if err != nil {
t.Fatal(err)
}
_, err = client.CreateFeed(&miniflux.FeedCreationRequest{
FeedURL: "",
CategoryID: categories[0].ID,
})
if err == nil {
t.Fatal(`Feeds should not be created with an empty feed URL`)
}
}
func TestCreateFeedWithInvalidFeedURL(t *testing.T) {
client := createClient(t)
categories, err := client.Categories()
if err != nil {
t.Fatal(err)
}
_, err = client.CreateFeed(&miniflux.FeedCreationRequest{
FeedURL: "invalid",
CategoryID: categories[0].ID,
})
if err == nil {
t.Fatal(`Feeds should not be created with an invalid feed URL`)
}
}
func TestCreateDisabledFeed(t *testing.T) { func TestCreateDisabledFeed(t *testing.T) {
client := createClient(t) client := createClient(t)
@ -174,7 +206,7 @@ func TestUpdateFeedURL(t *testing.T) {
client := createClient(t) client := createClient(t)
feed, _ := createFeed(t, client) feed, _ := createFeed(t, client)
url := "test" url := "https://www.example.org/feed.xml"
updatedFeed, err := client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{FeedURL: &url}) updatedFeed, err := client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{FeedURL: &url})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -183,15 +215,25 @@ func TestUpdateFeedURL(t *testing.T) {
if updatedFeed.FeedURL != url { if updatedFeed.FeedURL != url {
t.Fatalf(`Wrong FeedURL, got %q instead of %q`, updatedFeed.FeedURL, url) t.Fatalf(`Wrong FeedURL, got %q instead of %q`, updatedFeed.FeedURL, url)
} }
url = ""
updatedFeed, err = client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{FeedURL: &url})
if err != nil {
t.Fatal(err)
} }
if updatedFeed.FeedURL == "" { func TestUpdateFeedWithEmptyFeedURL(t *testing.T) {
t.Fatalf(`The FeedURL should not be empty`) client := createClient(t)
feed, _ := createFeed(t, client)
url := ""
if _, err := client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{FeedURL: &url}); err == nil {
t.Error(`Updating a feed with an empty feed URL should not be possible`)
}
}
func TestUpdateFeedWithInvalidFeedURL(t *testing.T) {
client := createClient(t)
feed, _ := createFeed(t, client)
url := "invalid"
if _, err := client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{FeedURL: &url}); err == nil {
t.Error(`Updating a feed with an invalid feed URL should not be possible`)
} }
} }
@ -199,7 +241,7 @@ func TestUpdateFeedSiteURL(t *testing.T) {
client := createClient(t) client := createClient(t)
feed, _ := createFeed(t, client) feed, _ := createFeed(t, client)
url := "test" url := "https://www.example.org/"
updatedFeed, err := client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{SiteURL: &url}) updatedFeed, err := client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{SiteURL: &url})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -208,15 +250,25 @@ func TestUpdateFeedSiteURL(t *testing.T) {
if updatedFeed.SiteURL != url { if updatedFeed.SiteURL != url {
t.Fatalf(`Wrong SiteURL, got %q instead of %q`, updatedFeed.SiteURL, url) t.Fatalf(`Wrong SiteURL, got %q instead of %q`, updatedFeed.SiteURL, url)
} }
url = ""
updatedFeed, err = client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{SiteURL: &url})
if err != nil {
t.Fatal(err)
} }
if updatedFeed.SiteURL == "" { func TestUpdateFeedWithEmptySiteURL(t *testing.T) {
t.Fatalf(`The SiteURL should not be empty`) client := createClient(t)
feed, _ := createFeed(t, client)
url := ""
if _, err := client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{SiteURL: &url}); err == nil {
t.Error(`Updating a feed with an empty site URL should not be possible`)
}
}
func TestUpdateFeedWithInvalidSiteURL(t *testing.T) {
client := createClient(t)
feed, _ := createFeed(t, client)
url := "invalid"
if _, err := client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{SiteURL: &url}); err == nil {
t.Error(`Updating a feed with an invalid site URL should not be possible`)
} }
} }
@ -233,15 +285,15 @@ func TestUpdateFeedTitle(t *testing.T) {
if updatedFeed.Title != newTitle { if updatedFeed.Title != newTitle {
t.Fatalf(`Wrong title, got %q instead of %q`, updatedFeed.Title, newTitle) t.Fatalf(`Wrong title, got %q instead of %q`, updatedFeed.Title, newTitle)
} }
newTitle = ""
updatedFeed, err = client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{Title: &newTitle})
if err != nil {
t.Fatal(err)
} }
if updatedFeed.Title == "" { func TestUpdateFeedWithEmptyTitle(t *testing.T) {
t.Fatalf(`The Title should not be empty`) client := createClient(t)
feed, _ := createFeed(t, client)
title := ""
if _, err := client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{Title: &title}); err == nil {
t.Error(`Updating a feed with an empty title should not be possible`)
} }
} }
@ -441,15 +493,25 @@ func TestUpdateFeedCategory(t *testing.T) {
if updatedFeed.Category.ID != newCategory.ID { if updatedFeed.Category.ID != newCategory.ID {
t.Fatalf(`Wrong CategoryID value, got "%v" instead of "%v"`, updatedFeed.Category.ID, newCategory.ID) t.Fatalf(`Wrong CategoryID value, got "%v" instead of "%v"`, updatedFeed.Category.ID, newCategory.ID)
} }
categoryID := int64(0)
updatedFeed, err = client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{CategoryID: &categoryID})
if err != nil {
t.Fatal(err)
} }
if updatedFeed.Category.ID == 0 { func TestUpdateFeedWithEmptyCategoryID(t *testing.T) {
t.Fatalf(`The CategoryID must defined`) client := createClient(t)
feed, _ := createFeed(t, client)
categoryID := int64(0)
if _, err := client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{CategoryID: &categoryID}); err == nil {
t.Error(`Updating a feed with an empty category should not be possible`)
}
}
func TestUpdateFeedWithInvalidCategoryID(t *testing.T) {
client := createClient(t)
feed, _ := createFeed(t, client)
categoryID := int64(-1)
if _, err := client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{CategoryID: &categoryID}); err == nil {
t.Error(`Updating a feed with an invalid category should not be possible`)
} }
} }

View file

@ -8,6 +8,8 @@ package tests
import ( import (
"testing" "testing"
miniflux "miniflux.app/client"
) )
func TestDiscoverSubscriptions(t *testing.T) { func TestDiscoverSubscriptions(t *testing.T) {
@ -33,3 +35,19 @@ func TestDiscoverSubscriptions(t *testing.T) {
t.Fatalf(`Invalid feed URL, got "%v" instead of "%v"`, subscriptions[0].URL, testFeedURL) t.Fatalf(`Invalid feed URL, got "%v" instead of "%v"`, subscriptions[0].URL, testFeedURL)
} }
} }
func TestDiscoverSubscriptionsWithInvalidURL(t *testing.T) {
client := createClient(t)
_, err := client.Discover("invalid")
if err == nil {
t.Fatal(`Invalid URLs should be rejected`)
}
}
func TestDiscoverSubscriptionsWithNoSubscription(t *testing.T) {
client := createClient(t)
_, err := client.Discover(testBaseURL)
if err != miniflux.ErrNotFound {
t.Fatal(`A 404 should be returned when there is no subscription`)
}
}

View file

@ -12,20 +12,22 @@ import (
"miniflux.app/http/response/html" "miniflux.app/http/response/html"
"miniflux.app/http/route" "miniflux.app/http/route"
"miniflux.app/logger" "miniflux.app/logger"
"miniflux.app/model"
"miniflux.app/ui/form" "miniflux.app/ui/form"
"miniflux.app/ui/session" "miniflux.app/ui/session"
"miniflux.app/ui/view" "miniflux.app/ui/view"
"miniflux.app/validator"
) )
func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) { func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
user, err := h.store.UserByID(request.UserID(r)) loggedUser, err := h.store.UserByID(request.UserID(r))
if err != nil { if err != nil {
html.ServerError(w, r, err) html.ServerError(w, r, err)
return return
} }
feedID := request.RouteInt64Param(r, "feedID") feedID := request.RouteInt64Param(r, "feedID")
feed, err := h.store.FeedByID(user.ID, feedID) feed, err := h.store.FeedByID(loggedUser.ID, feedID)
if err != nil { if err != nil {
html.ServerError(w, r, err) html.ServerError(w, r, err)
return return
@ -36,7 +38,7 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
return return
} }
categories, err := h.store.Categories(user.ID) categories, err := h.store.Categories(loggedUser.ID)
if err != nil { if err != nil {
html.ServerError(w, r, err) html.ServerError(w, r, err)
return return
@ -50,13 +52,20 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
view.Set("categories", categories) view.Set("categories", categories)
view.Set("feed", feed) view.Set("feed", feed)
view.Set("menu", "feeds") view.Set("menu", "feeds")
view.Set("user", user) view.Set("user", loggedUser)
view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) view.Set("countUnread", h.store.CountUnreadEntries(loggedUser.ID))
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(loggedUser.ID))
view.Set("defaultUserAgent", config.Opts.HTTPClientUserAgent()) view.Set("defaultUserAgent", config.Opts.HTTPClientUserAgent())
if err := feedForm.ValidateModification(); err != nil { feedModificationRequest := &model.FeedModificationRequest{
view.Set("errorMessage", err.Error()) FeedURL: model.OptionalString(feedForm.FeedURL),
SiteURL: model.OptionalString(feedForm.SiteURL),
Title: model.OptionalString(feedForm.Title),
CategoryID: model.OptionalInt64(feedForm.CategoryID),
}
if validationErr := validator.ValidateFeedModification(h.store, loggedUser.ID, feedModificationRequest); validationErr != nil {
view.Set("errorMessage", validationErr.TranslationKey)
html.OK(w, r, view.Render("edit_feed")) html.OK(w, r, view.Render("edit_feed"))
return return
} }

View file

@ -8,7 +8,6 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"miniflux.app/errors"
"miniflux.app/model" "miniflux.app/model"
) )
@ -31,14 +30,6 @@ type FeedForm struct {
Disabled bool Disabled bool
} }
// ValidateModification validates FeedForm fields
func (f FeedForm) ValidateModification() error {
if f.FeedURL == "" || f.SiteURL == "" || f.Title == "" || f.CategoryID == 0 {
return errors.NewLocalizedError("error.fields_mandatory")
}
return nil
}
// Merge updates the fields of the given feed. // Merge updates the fields of the given feed.
func (f FeedForm) Merge(feed *model.Feed) *model.Feed { func (f FeedForm) Merge(feed *model.Feed) *model.Feed {
feed.Category.ID = f.CategoryID feed.Category.ID = f.CategoryID

View file

@ -11,6 +11,7 @@ import (
"miniflux.app/http/request" "miniflux.app/http/request"
"miniflux.app/http/response/html" "miniflux.app/http/response/html"
"miniflux.app/http/route" "miniflux.app/http/route"
"miniflux.app/model"
feedHandler "miniflux.app/reader/handler" feedHandler "miniflux.app/reader/handler"
"miniflux.app/ui/form" "miniflux.app/ui/form"
"miniflux.app/ui/session" "miniflux.app/ui/session"
@ -48,8 +49,7 @@ func (h *handler) showChooseSubscriptionPage(w http.ResponseWriter, r *http.Requ
return return
} }
feed, err := feedHandler.CreateFeed(h.store, &feedHandler.FeedCreationArgs{ feed, err := feedHandler.CreateFeed(h.store, user.ID, &model.FeedCreationRequest{
UserID: user.ID,
CategoryID: subscriptionForm.CategoryID, CategoryID: subscriptionForm.CategoryID,
FeedURL: subscriptionForm.URL, FeedURL: subscriptionForm.URL,
Crawler: subscriptionForm.Crawler, Crawler: subscriptionForm.Crawler,

View file

@ -12,6 +12,7 @@ import (
"miniflux.app/http/response/html" "miniflux.app/http/response/html"
"miniflux.app/http/route" "miniflux.app/http/route"
"miniflux.app/logger" "miniflux.app/logger"
"miniflux.app/model"
feedHandler "miniflux.app/reader/handler" feedHandler "miniflux.app/reader/handler"
"miniflux.app/reader/subscription" "miniflux.app/reader/subscription"
"miniflux.app/ui/form" "miniflux.app/ui/form"
@ -75,8 +76,7 @@ func (h *handler) submitSubscription(w http.ResponseWriter, r *http.Request) {
v.Set("errorMessage", "error.subscription_not_found") v.Set("errorMessage", "error.subscription_not_found")
html.OK(w, r, v.Render("add_subscription")) html.OK(w, r, v.Render("add_subscription"))
case n == 1: case n == 1:
feed, err := feedHandler.CreateFeed(h.store, &feedHandler.FeedCreationArgs{ feed, err := feedHandler.CreateFeed(h.store, user.ID, &model.FeedCreationRequest{
UserID: user.ID,
CategoryID: subscriptionForm.CategoryID, CategoryID: subscriptionForm.CategoryID,
FeedURL: subscriptions[0].URL, FeedURL: subscriptions[0].URL,
Crawler: subscriptionForm.Crawler, Crawler: subscriptionForm.Crawler,

68
validator/feed.go Normal file
View file

@ -0,0 +1,68 @@
// Copyright 2021 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package validator // import "miniflux.app/validator"
import (
"miniflux.app/model"
"miniflux.app/storage"
)
// ValidateFeedCreation validates feed creation.
func ValidateFeedCreation(store *storage.Storage, userID int64, request *model.FeedCreationRequest) *ValidationError {
if request.FeedURL == "" || request.CategoryID <= 0 {
return NewValidationError("error.feed_mandatory_fields")
}
if !isValidURL(request.FeedURL) {
return NewValidationError("error.invalid_feed_url")
}
if store.FeedURLExists(userID, request.FeedURL) {
return NewValidationError("error.feed_already_exists")
}
if !store.CategoryIDExists(userID, request.CategoryID) {
return NewValidationError("error.feed_category_not_found")
}
return nil
}
// ValidateFeedModification validates feed modification.
func ValidateFeedModification(store *storage.Storage, userID int64, request *model.FeedModificationRequest) *ValidationError {
if request.FeedURL != nil {
if *request.FeedURL == "" {
return NewValidationError("error.feed_url_not_empty")
}
if !isValidURL(*request.FeedURL) {
return NewValidationError("error.invalid_feed_url")
}
}
if request.SiteURL != nil {
if *request.SiteURL == "" {
return NewValidationError("error.site_url_not_empty")
}
if !isValidURL(*request.SiteURL) {
return NewValidationError("error.invalid_site_url")
}
}
if request.Title != nil {
if *request.Title == "" {
return NewValidationError("error.feed_title_not_empty")
}
}
if request.CategoryID != nil {
if !store.CategoryIDExists(userID, *request.CategoryID) {
return NewValidationError("error.feed_category_not_found")
}
}
return nil
}

16
validator/subscription.go Normal file
View file

@ -0,0 +1,16 @@
// Copyright 2021 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package validator // import "miniflux.app/validator"
import "miniflux.app/model"
// ValidateSubscriptionDiscovery validates subscription discovery requests.
func ValidateSubscriptionDiscovery(request *model.SubscriptionDiscoveryRequest) *ValidationError {
if !isValidURL(request.URL) {
return NewValidationError("error.invalid_site_url")
}
return nil
}

View file

@ -6,6 +6,7 @@ package validator // import "miniflux.app/validator"
import ( import (
"errors" "errors"
"net/url"
"miniflux.app/locale" "miniflux.app/locale"
) )
@ -27,3 +28,8 @@ func (v *ValidationError) String() string {
func (v *ValidationError) Error() error { func (v *ValidationError) Error() error {
return errors.New(v.String()) return errors.New(v.String())
} }
func isValidURL(absoluteURL string) bool {
_, err := url.ParseRequestURI(absoluteURL)
return err == nil
}

View file

@ -0,0 +1,22 @@
// Copyright 2021 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package validator // import "miniflux.app/validator"
import "testing"
func TestIsValidURL(t *testing.T) {
scenarios := map[string]bool{
"https://www.example.org": true,
"http://www.example.org/": true,
"www.example.org": false,
}
for link, expected := range scenarios {
result := isValidURL(link)
if result != expected {
t.Errorf(`Unexpected result, got %v instead of %v`, result, expected)
}
}
}