Refactor feed validator
This commit is contained in:
parent
b35fece3d5
commit
806b9545a9
32 changed files with 588 additions and 521 deletions
|
@ -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.removeCategory).Methods(http.MethodDelete)
|
||||
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.getFeeds).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/feeds/refresh", handler.refreshAllFeeds).Methods(http.MethodPut)
|
||||
|
|
61
api/feed.go
61
api/feed.go
|
@ -5,60 +5,32 @@
|
|||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
json_parser "encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/model"
|
||||
feedHandler "miniflux.app/reader/handler"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
func (h *handler) createFeed(w http.ResponseWriter, r *http.Request) {
|
||||
feedInfo, err := decodeFeedCreationRequest(r.Body)
|
||||
if err != nil {
|
||||
userID := request.UserID(r)
|
||||
|
||||
var feedCreationRequest model.FeedCreationRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&feedCreationRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if feedInfo.FeedURL == "" {
|
||||
json.BadRequest(w, r, errors.New("The feed_url is required"))
|
||||
if validationErr := validator.ValidateFeedCreation(h.store, userID, &feedCreationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if feedInfo.CategoryID <= 0 {
|
||||
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,
|
||||
})
|
||||
feed, err := feedHandler.CreateFeed(h.store, userID, &feedCreationRequest)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
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) {
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
feedChanges, err := decodeFeedModificationRequest(r.Body)
|
||||
if err != nil {
|
||||
var feedModificationRequest model.FeedModificationRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&feedModificationRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
userID := request.UserID(r)
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
|
||||
originalFeed, err := h.store.FeedByID(userID, feedID)
|
||||
if err != nil {
|
||||
|
@ -121,13 +93,12 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
feedChanges.Update(originalFeed)
|
||||
|
||||
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"))
|
||||
if validationErr := validator.ValidateFeedModification(h.store, userID, &feedModificationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
feedModificationRequest.Patch(originalFeed)
|
||||
if err := h.store.UpdateFeed(originalFeed); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
|
|
140
api/payload.go
140
api/payload.go
|
@ -23,150 +23,10 @@ type entriesResponse struct {
|
|||
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 {
|
||||
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) {
|
||||
type payload struct {
|
||||
EntryIDs []int64 `json:"entry_ids"`
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -5,25 +5,33 @@
|
|||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
json_parser "encoding/json"
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/reader/subscription"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
func (h *handler) getSubscriptions(w http.ResponseWriter, r *http.Request) {
|
||||
subscriptionRequest, bodyErr := decodeSubscriptionDiscoveryRequest(r.Body)
|
||||
if bodyErr != nil {
|
||||
json.BadRequest(w, r, bodyErr)
|
||||
func (h *handler) discoverSubscriptions(w http.ResponseWriter, r *http.Request) {
|
||||
var subscriptionDiscoveryRequest model.SubscriptionDiscoveryRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&subscriptionDiscoveryRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateSubscriptionDiscovery(&subscriptionDiscoveryRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
subscriptions, finderErr := subscription.FindSubscriptions(
|
||||
subscriptionRequest.URL,
|
||||
subscriptionRequest.UserAgent,
|
||||
subscriptionRequest.Username,
|
||||
subscriptionRequest.Password,
|
||||
subscriptionRequest.FetchViaProxy,
|
||||
subscriptionDiscoveryRequest.URL,
|
||||
subscriptionDiscoveryRequest.UserAgent,
|
||||
subscriptionDiscoveryRequest.Username,
|
||||
subscriptionDiscoveryRequest.Password,
|
||||
subscriptionDiscoveryRequest.FetchViaProxy,
|
||||
)
|
||||
if finderErr != nil {
|
||||
json.ServerError(w, r, finderErr)
|
||||
|
|
|
@ -314,8 +314,7 @@ func (c *Client) UpdateFeed(feedID int64, feedChanges *FeedModificationRequest)
|
|||
defer body.Close()
|
||||
|
||||
var f *Feed
|
||||
decoder := json.NewDecoder(body)
|
||||
if err := decoder.Decode(&f); err != nil {
|
||||
if err := json.NewDecoder(body).Decode(&f); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -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.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_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.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.",
|
||||
|
@ -599,6 +606,13 @@ var translations = map[string]string{
|
|||
"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.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.api_key_already_exists": "This API Key already exists.",
|
||||
"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.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_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.api_key_already_exists": "Esta clave API ya existe.",
|
||||
"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.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_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.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.",
|
||||
|
@ -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.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_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.api_key_already_exists": "Questa chiave API esiste già.",
|
||||
"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.entries_per_page_invalid": "ページあたりのエントリ数が無効です。",
|
||||
"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.api_key_already_exists": "この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.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_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.api_key_already_exists": "This API Key already exists.",
|
||||
"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.entries_per_page_invalid": "Liczba wpisów na stronę jest nieprawidłowa.",
|
||||
"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.api_key_already_exists": "Deze API-sleutel bestaat al.",
|
||||
"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.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_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.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.",
|
||||
|
@ -3333,6 +3396,13 @@ var translations = map[string]string{
|
|||
"error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.",
|
||||
"error.entries_per_page_invalid": "Количество записей на странице недействительно.",
|
||||
"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.api_key_already_exists": "Этот ключ API уже существует.",
|
||||
"error.unable_to_create_api_key": "Невозможно создать этот ключ API.",
|
||||
|
@ -3671,6 +3741,13 @@ var translations = map[string]string{
|
|||
"error.settings_mandatory_fields": "必须填写用户名、主题、语言以及时区",
|
||||
"error.entries_per_page_invalid": "每页的条目数无效。",
|
||||
"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.api_key_already_exists": "此API密钥已存在。",
|
||||
"error.unable_to_create_api_key": "无法创建此API密钥。",
|
||||
|
@ -3783,15 +3860,15 @@ var translations = map[string]string{
|
|||
}
|
||||
|
||||
var translationsChecksums = map[string]string{
|
||||
"de_DE": "c8d6021599cfda4f853bd5ec1e1b065f03633ada9211ee22879ea778ba464572",
|
||||
"en_US": "781a7a6b54f439d76fe56fca7cb07412a04e71edebf53563f5cca27a0cd2533a",
|
||||
"es_ES": "4d602461f5ed9c4aaf59e8828d2b09d0cc45d06ba77d89ba0ef9662b580aebc0",
|
||||
"fr_FR": "3a0a008d0857fa5eb8a018ce5e348d7ccabe08a67849c72c6e7611e6b5b49aa7",
|
||||
"it_IT": "7222e3610ad3741aa7aff957f70524b63ffe3f6729198899231765335861a108",
|
||||
"ja_JP": "f0ab6dd77c78717d25d88baad39c487c913720be4b3473a3f0aa3aa538318deb",
|
||||
"nl_NL": "1e0872b89fb78a6de2ae989d054963226146c3eeff4b2883cf2bf8df96c13846",
|
||||
"pl_PL": "2513808a13925549c9ba27c52a20916d18a5222dd8ba6a14520798766889b076",
|
||||
"pt_BR": "b5fc4d9e0dedc554579154f2fff772b108baf317c9a952d688db0df260674b3b",
|
||||
"ru_RU": "d9bedead0757deae57da909c7d5297853c2186acb8ebf7cf91d0eef7c1a17d19",
|
||||
"zh_CN": "2526d0139ca0a2004f2db0864cbc9c3da55c3c7f45e1a244fea3c39d5d39e0f9",
|
||||
"de_DE": "9db01e4337375c37edae008d93ccf8f1ab50f6b30a468cefe75257e5b1364513",
|
||||
"en_US": "506dc83a66e38147328f924b15f1280a71fb3cc5f83c1f690c6029d137f8baee",
|
||||
"es_ES": "37c7d271dcae76f524a8e52b86bfaa9dc680954ba75ed53e7945b95ffe2ae9e9",
|
||||
"fr_FR": "ed626b9752239c0f89a17ff28c5003a5ab930a3f0d1df5628b23e8de3587c0f5",
|
||||
"it_IT": "015892b01bc85407a0813eb60deb1a1bbbfaf2b72bb9194e13378083f6f54b84",
|
||||
"ja_JP": "ea1843af4638ce58cfe4ca730133e7ef178c6242b6bd253c714b179b45efde9f",
|
||||
"nl_NL": "fe3d9e519d3326d0ff51590011ac6cb344e26e0aa241a8295fb38ca7a7c2191c",
|
||||
"pl_PL": "5af4497ab4420ff8cec45b86dc65ddc685cd9cca0fb750e238d57f1a8d43c32f",
|
||||
"pt_BR": "052cfe35211165ed7de9e99109a819d362b5a69b490bb58cc6d884e8fbbf4469",
|
||||
"ru_RU": "c7216ede62f1c1d18b2ad05bb20a2dfdab04e7bb968773a705da8c26cd5bdcd8",
|
||||
"zh_CN": "8e02550d068e3d8020bd7923a00e5a045dd09db1cc0dfdaa2294417175068743",
|
||||
}
|
||||
|
|
|
@ -236,6 +236,13 @@
|
|||
"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.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.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.",
|
||||
|
|
|
@ -240,6 +240,13 @@
|
|||
"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.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.api_key_already_exists": "This API Key already exists.",
|
||||
"error.unable_to_create_api_key": "Unable to create this API Key.",
|
||||
|
|
|
@ -236,6 +236,13 @@
|
|||
"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.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.api_key_already_exists": "Esta clave API ya existe.",
|
||||
"error.unable_to_create_api_key": "No se puede crear esta clave API.",
|
||||
|
|
|
@ -236,6 +236,13 @@
|
|||
"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.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.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.",
|
||||
|
|
|
@ -236,6 +236,13 @@
|
|||
"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.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.api_key_already_exists": "Questa chiave API esiste già.",
|
||||
"error.unable_to_create_api_key": "Impossibile creare questa chiave API.",
|
||||
|
|
|
@ -236,6 +236,13 @@
|
|||
"error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンの全てが必要です。",
|
||||
"error.entries_per_page_invalid": "ページあたりのエントリ数が無効です。",
|
||||
"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.api_key_already_exists": "このAPIキーは既に存在します。",
|
||||
"error.unable_to_create_api_key": "このAPIキーを作成できません。",
|
||||
|
|
|
@ -236,6 +236,13 @@
|
|||
"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.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.api_key_already_exists": "This API Key already exists.",
|
||||
"error.unable_to_create_api_key": "Kan deze API-sleutel niet maken.",
|
||||
|
|
|
@ -238,6 +238,13 @@
|
|||
"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.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.api_key_already_exists": "Deze API-sleutel bestaat al.",
|
||||
"error.unable_to_create_api_key": "Nie można utworzyć tego klucza API.",
|
||||
|
|
|
@ -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.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_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.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.",
|
||||
|
|
|
@ -238,6 +238,13 @@
|
|||
"error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.",
|
||||
"error.entries_per_page_invalid": "Количество записей на странице недействительно.",
|
||||
"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.api_key_already_exists": "Этот ключ API уже существует.",
|
||||
"error.unable_to_create_api_key": "Невозможно создать этот ключ API.",
|
||||
|
|
|
@ -234,6 +234,13 @@
|
|||
"error.settings_mandatory_fields": "必须填写用户名、主题、语言以及时区",
|
||||
"error.entries_per_page_invalid": "每页的条目数无效。",
|
||||
"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.api_key_already_exists": "此API密钥已存在。",
|
||||
"error.unable_to_create_api_key": "无法创建此API密钥。",
|
||||
|
|
111
model/feed.go
111
model/feed.go
|
@ -13,6 +13,12 @@ import (
|
|||
"miniflux.app/http/client"
|
||||
)
|
||||
|
||||
// List of supported schedulers.
|
||||
const (
|
||||
SchedulerRoundRobin = "round_robin"
|
||||
SchedulerEntryFrequency = "entry_frequency"
|
||||
)
|
||||
|
||||
// Feed represents a feed in the application.
|
||||
type Feed struct {
|
||||
ID int64 `json:"id"`
|
||||
|
@ -44,12 +50,6 @@ type Feed struct {
|
|||
ReadCount int `json:"-"`
|
||||
}
|
||||
|
||||
// List of supported schedulers.
|
||||
const (
|
||||
SchedulerRoundRobin = "round_robin"
|
||||
SchedulerEntryFrequency = "entry_frequency"
|
||||
)
|
||||
|
||||
func (f *Feed) String() string {
|
||||
return fmt.Sprintf("ID=%d, UserID=%d, FeedURL=%s, SiteURL=%s, Title=%s, Category={%s}",
|
||||
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
|
||||
type Feeds []*Feed
|
||||
|
|
|
@ -19,3 +19,11 @@ func OptionalInt(value int) *int {
|
|||
}
|
||||
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
14
model/subscription.go
Normal 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"`
|
||||
}
|
|
@ -28,37 +28,19 @@ var (
|
|||
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.
|
||||
func CreateFeed(store *storage.Storage, args *FeedCreationArgs) (*model.Feed, error) {
|
||||
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[CreateFeed] FeedURL=%s", args.FeedURL))
|
||||
func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model.FeedCreationRequest) (*model.Feed, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
request := client.NewClientWithConfig(args.FeedURL, config.Opts)
|
||||
request.WithCredentials(args.Username, args.Password)
|
||||
request.WithUserAgent(args.UserAgent)
|
||||
request := client.NewClientWithConfig(feedCreationRequest.FeedURL, config.Opts)
|
||||
request.WithCredentials(feedCreationRequest.Username, feedCreationRequest.Password)
|
||||
request.WithUserAgent(feedCreationRequest.UserAgent)
|
||||
|
||||
if args.FetchViaProxy {
|
||||
if feedCreationRequest.FetchViaProxy {
|
||||
request.WithProxy()
|
||||
}
|
||||
|
||||
|
@ -67,7 +49,7 @@ func CreateFeed(store *storage.Storage, args *FeedCreationArgs) (*model.Feed, er
|
|||
return nil, requestErr
|
||||
}
|
||||
|
||||
if store.FeedURLExists(args.UserID, response.EffectiveURL) {
|
||||
if store.FeedURLExists(userID, 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
|
||||
}
|
||||
|
||||
subscription.UserID = args.UserID
|
||||
subscription.UserAgent = args.UserAgent
|
||||
subscription.Username = args.Username
|
||||
subscription.Password = args.Password
|
||||
subscription.Crawler = args.Crawler
|
||||
subscription.Disabled = args.Disabled
|
||||
subscription.IgnoreHTTPCache = args.IgnoreHTTPCache
|
||||
subscription.FetchViaProxy = args.FetchViaProxy
|
||||
subscription.ScraperRules = args.ScraperRules
|
||||
subscription.RewriteRules = args.RewriteRules
|
||||
subscription.BlocklistRules = args.BlocklistRules
|
||||
subscription.KeeplistRules = args.KeeplistRules
|
||||
subscription.WithCategoryID(args.CategoryID)
|
||||
subscription.UserID = userID
|
||||
subscription.UserAgent = feedCreationRequest.UserAgent
|
||||
subscription.Username = feedCreationRequest.Username
|
||||
subscription.Password = feedCreationRequest.Password
|
||||
subscription.Crawler = feedCreationRequest.Crawler
|
||||
subscription.Disabled = feedCreationRequest.Disabled
|
||||
subscription.IgnoreHTTPCache = feedCreationRequest.IgnoreHTTPCache
|
||||
subscription.FetchViaProxy = feedCreationRequest.FetchViaProxy
|
||||
subscription.ScraperRules = feedCreationRequest.ScraperRules
|
||||
subscription.RewriteRules = feedCreationRequest.RewriteRules
|
||||
subscription.BlocklistRules = feedCreationRequest.BlocklistRules
|
||||
subscription.KeeplistRules = feedCreationRequest.KeeplistRules
|
||||
subscription.WithCategoryID(feedCreationRequest.CategoryID)
|
||||
subscription.WithClientResponse(response)
|
||||
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)
|
||||
|
||||
checkFeedIcon(store, subscription.ID, subscription.SiteURL, args.FetchViaProxy)
|
||||
checkFeedIcon(store, subscription.ID, subscription.SiteURL, feedCreationRequest.FetchViaProxy)
|
||||
return subscription, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
client := createClient(t)
|
||||
|
||||
|
@ -174,7 +206,7 @@ func TestUpdateFeedURL(t *testing.T) {
|
|||
client := createClient(t)
|
||||
feed, _ := createFeed(t, client)
|
||||
|
||||
url := "test"
|
||||
url := "https://www.example.org/feed.xml"
|
||||
updatedFeed, err := client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{FeedURL: &url})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -183,15 +215,25 @@ func TestUpdateFeedURL(t *testing.T) {
|
|||
if 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)
|
||||
func TestUpdateFeedWithEmptyFeedURL(t *testing.T) {
|
||||
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`)
|
||||
}
|
||||
}
|
||||
|
||||
if updatedFeed.FeedURL == "" {
|
||||
t.Fatalf(`The FeedURL should not be empty`)
|
||||
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)
|
||||
feed, _ := createFeed(t, client)
|
||||
|
||||
url := "test"
|
||||
url := "https://www.example.org/"
|
||||
updatedFeed, err := client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{SiteURL: &url})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -208,15 +250,25 @@ func TestUpdateFeedSiteURL(t *testing.T) {
|
|||
if 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)
|
||||
func TestUpdateFeedWithEmptySiteURL(t *testing.T) {
|
||||
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`)
|
||||
}
|
||||
}
|
||||
|
||||
if updatedFeed.SiteURL == "" {
|
||||
t.Fatalf(`The SiteURL should not be empty`)
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
func TestUpdateFeedWithEmptyTitle(t *testing.T) {
|
||||
client := createClient(t)
|
||||
feed, _ := createFeed(t, client)
|
||||
|
||||
if updatedFeed.Title == "" {
|
||||
t.Fatalf(`The Title should not be empty`)
|
||||
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 {
|
||||
t.Fatalf(`Wrong CategoryID value, got "%v" instead of "%v"`, updatedFeed.Category.ID, newCategory.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeedWithEmptyCategoryID(t *testing.T) {
|
||||
client := createClient(t)
|
||||
feed, _ := createFeed(t, client)
|
||||
|
||||
categoryID := int64(0)
|
||||
updatedFeed, err = client.UpdateFeed(feed.ID, &miniflux.FeedModificationRequest{CategoryID: &categoryID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
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`)
|
||||
}
|
||||
}
|
||||
|
||||
if updatedFeed.Category.ID == 0 {
|
||||
t.Fatalf(`The CategoryID must defined`)
|
||||
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`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ package tests
|
|||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
miniflux "miniflux.app/client"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,20 +12,22 @@ import (
|
|||
"miniflux.app/http/response/html"
|
||||
"miniflux.app/http/route"
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/ui/form"
|
||||
"miniflux.app/ui/session"
|
||||
"miniflux.app/ui/view"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
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 {
|
||||
html.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
feed, err := h.store.FeedByID(user.ID, feedID)
|
||||
feed, err := h.store.FeedByID(loggedUser.ID, feedID)
|
||||
if err != nil {
|
||||
html.ServerError(w, r, err)
|
||||
return
|
||||
|
@ -36,7 +38,7 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
categories, err := h.store.Categories(user.ID)
|
||||
categories, err := h.store.Categories(loggedUser.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, r, err)
|
||||
return
|
||||
|
@ -50,13 +52,20 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
|
|||
view.Set("categories", categories)
|
||||
view.Set("feed", feed)
|
||||
view.Set("menu", "feeds")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
|
||||
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
|
||||
view.Set("user", loggedUser)
|
||||
view.Set("countUnread", h.store.CountUnreadEntries(loggedUser.ID))
|
||||
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(loggedUser.ID))
|
||||
view.Set("defaultUserAgent", config.Opts.HTTPClientUserAgent())
|
||||
|
||||
if err := feedForm.ValidateModification(); err != nil {
|
||||
view.Set("errorMessage", err.Error())
|
||||
feedModificationRequest := &model.FeedModificationRequest{
|
||||
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"))
|
||||
return
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"miniflux.app/errors"
|
||||
"miniflux.app/model"
|
||||
)
|
||||
|
||||
|
@ -31,14 +30,6 @@ type FeedForm struct {
|
|||
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.
|
||||
func (f FeedForm) Merge(feed *model.Feed) *model.Feed {
|
||||
feed.Category.ID = f.CategoryID
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/html"
|
||||
"miniflux.app/http/route"
|
||||
"miniflux.app/model"
|
||||
feedHandler "miniflux.app/reader/handler"
|
||||
"miniflux.app/ui/form"
|
||||
"miniflux.app/ui/session"
|
||||
|
@ -48,8 +49,7 @@ func (h *handler) showChooseSubscriptionPage(w http.ResponseWriter, r *http.Requ
|
|||
return
|
||||
}
|
||||
|
||||
feed, err := feedHandler.CreateFeed(h.store, &feedHandler.FeedCreationArgs{
|
||||
UserID: user.ID,
|
||||
feed, err := feedHandler.CreateFeed(h.store, user.ID, &model.FeedCreationRequest{
|
||||
CategoryID: subscriptionForm.CategoryID,
|
||||
FeedURL: subscriptionForm.URL,
|
||||
Crawler: subscriptionForm.Crawler,
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"miniflux.app/http/response/html"
|
||||
"miniflux.app/http/route"
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/model"
|
||||
feedHandler "miniflux.app/reader/handler"
|
||||
"miniflux.app/reader/subscription"
|
||||
"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")
|
||||
html.OK(w, r, v.Render("add_subscription"))
|
||||
case n == 1:
|
||||
feed, err := feedHandler.CreateFeed(h.store, &feedHandler.FeedCreationArgs{
|
||||
UserID: user.ID,
|
||||
feed, err := feedHandler.CreateFeed(h.store, user.ID, &model.FeedCreationRequest{
|
||||
CategoryID: subscriptionForm.CategoryID,
|
||||
FeedURL: subscriptions[0].URL,
|
||||
Crawler: subscriptionForm.Crawler,
|
||||
|
|
68
validator/feed.go
Normal file
68
validator/feed.go
Normal 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
16
validator/subscription.go
Normal 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
|
||||
}
|
|
@ -6,6 +6,7 @@ package validator // import "miniflux.app/validator"
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"miniflux.app/locale"
|
||||
)
|
||||
|
@ -27,3 +28,8 @@ func (v *ValidationError) String() string {
|
|||
func (v *ValidationError) Error() error {
|
||||
return errors.New(v.String())
|
||||
}
|
||||
|
||||
func isValidURL(absoluteURL string) bool {
|
||||
_, err := url.ParseRequestURI(absoluteURL)
|
||||
return err == nil
|
||||
}
|
||||
|
|
22
validator/validator_test.go
Normal file
22
validator/validator_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue