ソースを参照

Validate Keep list and Block list rules syntax

Frédéric Guillot 5 年 前
コミット
e8d0360e64

+ 34 - 12
locale/translations.go

@@ -249,6 +249,8 @@ var translations = map[string]string{
     "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.feed_invalid_blocklist_rule": "Die Sperrlistenregel ist ungültig.",
+    "error.feed_invalid_keeplist_rule": "Die Keep List-Regel ist ungültig.",
     "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.",
@@ -615,6 +617,8 @@ var translations = map[string]string{
     "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.feed_invalid_blocklist_rule": "The block list rule is invalid.",
+    "error.feed_invalid_keeplist_rule": "The keep list rule is invalid.",
     "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.",
@@ -953,6 +957,8 @@ var translations = map[string]string{
     "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.feed_invalid_blocklist_rule": "La regla de la lista de bloqueo no es válida.",
+    "error.feed_invalid_keeplist_rule": "La regla de mantener la lista no es válida.",
     "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.",
@@ -1295,6 +1301,8 @@ var translations = map[string]string{
     "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.feed_invalid_blocklist_rule": "La règle de blocage n'est pas valide.",
+    "error.feed_invalid_keeplist_rule": "La règle d'autorisation n'est pas valide.",
     "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.",
@@ -1314,7 +1322,7 @@ var translations = map[string]string{
     "form.feed.label.rewrite_rules": "Règles de réécriture",
     "form.feed.label.blocklist_rules": "Règles de blocage",
     "form.feed.label.keeplist_rules": "Règles d'autorisation",
-    "form.feed.label.ignore_http_cache": "Ignore cache HTTP",
+    "form.feed.label.ignore_http_cache": "Ignorer le cache HTTP",
     "form.feed.label.fetch_via_proxy": "Récupérer via proxy",
     "form.feed.label.disabled": "Ne pas actualiser ce flux",
     "form.category.label.title": "Titre",
@@ -1657,6 +1665,8 @@ var translations = map[string]string{
     "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.feed_invalid_blocklist_rule": "La regola dell'elenco di blocco non è valida.",
+    "error.feed_invalid_keeplist_rule": "La regola dell'elenco di conservazione non è valida.",
     "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.",
@@ -1999,6 +2009,8 @@ var translations = map[string]string{
     "error.site_url_not_empty": "サイトのURLを空にすることはできません。",
     "error.feed_title_not_empty": "フィードのタイトルを空にすることはできません。",
     "error.feed_category_not_found": "このカテゴリは存在しないか、このユーザーに属していません。",
+    "error.feed_invalid_blocklist_rule": "ブロックリストルールが無効です。",
+    "error.feed_invalid_keeplist_rule": "リストの保持ルールが無効です。",
     "error.user_mandatory_fields": "ユーザー名が必要です。",
     "error.api_key_already_exists": "このAPIキーは既に存在します。",
     "error.unable_to_create_api_key": "このAPIキーを作成できません。",
@@ -2341,6 +2353,8 @@ var translations = map[string]string{
     "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.feed_invalid_blocklist_rule": "De regel voor de blokkeerlijst is ongeldig.",
+    "error.feed_invalid_keeplist_rule": "De regel voor het bewaren van een lijst is ongeldig.",
     "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.",
@@ -2703,6 +2717,8 @@ var translations = map[string]string{
     "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.feed_invalid_blocklist_rule": "Reguła listy zablokowanych jest nieprawidłowa.",
+    "error.feed_invalid_keeplist_rule": "Reguła listy zachowania jest nieprawidłowa.",
     "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.",
@@ -3069,6 +3085,8 @@ var translations = map[string]string{
     "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.feed_invalid_blocklist_rule": "A regra da lista de bloqueio é inválida.",
+    "error.feed_invalid_keeplist_rule": "A regra de manutenção da lista é inválida.",
     "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.",
@@ -3413,6 +3431,8 @@ var translations = map[string]string{
     "error.site_url_not_empty": "URL сайта не может быть пустым.",
     "error.feed_title_not_empty": "Заголовок фида не может быть пустым.",
     "error.feed_category_not_found": "Эта категория не существует или не принадлежит этому пользователю.",
+    "error.feed_invalid_blocklist_rule": "Правило черного списка недействительно.",
+    "error.feed_invalid_keeplist_rule": "Правило списка хранения недействительно.",
     "error.user_mandatory_fields": "Имя пользователя обязательно.",
     "error.api_key_already_exists": "Этот ключ API уже существует.",
     "error.unable_to_create_api_key": "Невозможно создать этот ключ API.",
@@ -3759,6 +3779,8 @@ var translations = map[string]string{
     "error.site_url_not_empty": "网站网址不能为空。",
     "error.feed_title_not_empty": "供稿标题不能为空。",
     "error.feed_category_not_found": "此类别不存在或不属于该用户。",
+    "error.feed_invalid_blocklist_rule": "阻止列表规则无效。",
+    "error.feed_invalid_keeplist_rule": "保留列表规则无效。",
     "error.user_mandatory_fields": "必须填写用户名",
     "error.api_key_already_exists": "此API密钥已存在。",
     "error.unable_to_create_api_key": "无法创建此API密钥。",
@@ -3871,15 +3893,15 @@ var translations = map[string]string{
 }
 
 var translationsChecksums = map[string]string{
-	"de_DE": "96616242d64bfca6bcc27762260aac38a3014ae854dfe4d85711491f7e0e52a2",
-	"en_US": "0718463b2edc0157dba7b26c033bcfa36181e57addd613a19ce60e6c6b34cb19",
-	"es_ES": "fc767762d1eb5cd44d89aab99a58a0c28b910ba06457d7ffe1b21092d473bb8e",
-	"fr_FR": "3abdb817b699ffb2c9f3c2316879c65b96510ff3e6a875860fcccdd063b29e63",
-	"it_IT": "2d2aa46e81f48494c08b78963d88d6522395856bd25f4b1504403da1f8d531e8",
-	"ja_JP": "7c1098e38962f4d285b957f7db428b28d562d1fe3b16117c0ac0340870cd43b0",
-	"nl_NL": "43093afdfa7d692d83455f5e4f3d36d4e725c704796961b749a84ba27ddbbd0b",
-	"pl_PL": "eb0baf99b46f5440a32084453091272f57c5f676b595640016f168fb65e439b7",
-	"pt_BR": "9ed1a472f60e65c4f8309417a3af17572299cd23c25ae6e1d368996578963b25",
-	"ru_RU": "de2cda1638754b7c3376749fa7d62d75d3594543465c1f78f801fe6c08d4eb2f",
-	"zh_CN": "07f1fb9efeb93f2d45b71c7028b50fe5bca378c307d1e2dadd0821fabd074918",
+	"de_DE": "204b8359228926129612defadb82b425ca6d76a60606572a5e66b4313753b6b8",
+	"en_US": "836c34115c6e190e9b77e894ab998193bdc5dae204636084994a09b6274ff41f",
+	"es_ES": "b190315004d23bfeda4123e6f77613bdcb78335b0b6a2876dd0e1bc76c428eaa",
+	"fr_FR": "7be464741f4301dd3a6ace3fd66f471884dbbd1f9a2866a805502227382b94a7",
+	"it_IT": "e2027bf6c5ee384d0bf776c4fb04fd99b5c4e18c694db4d7ff09529b9fe96d17",
+	"ja_JP": "33ac3c96003b95222dc1318f6dc1e929c8b75e2415700e7b544abfbe57b6eb0a",
+	"nl_NL": "dbf1343301ed35161f8deebd25726bc39d6a1bfdeb198ac555b1a0c72e16d96d",
+	"pl_PL": "8c646402462d62235245229f3a9f0297cdf6ac2f53df1e25508075e43a1ccf4e",
+	"pt_BR": "2d613ce5fd8b00da9df5c9cf1f675839cdfc3e4c5646b0cc949f0bf18aca59d0",
+	"ru_RU": "e6c4d0467051235d28c0329404e9ec8a7d353563b789f69e6cada2099d816f5d",
+	"zh_CN": "51ec39c564c11373b1574ed35d798dab61036d8cbeb6c807116e5dbf7443f31c",
 }

+ 2 - 0
locale/translations/de_DE.json

@@ -244,6 +244,8 @@
     "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.feed_invalid_blocklist_rule": "Die Sperrlistenregel ist ungültig.",
+    "error.feed_invalid_keeplist_rule": "Die Keep List-Regel ist ungültig.",
     "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.",

+ 2 - 0
locale/translations/en_US.json

@@ -248,6 +248,8 @@
     "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.feed_invalid_blocklist_rule": "The block list rule is invalid.",
+    "error.feed_invalid_keeplist_rule": "The keep list rule is invalid.",
     "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.",

+ 2 - 0
locale/translations/es_ES.json

@@ -244,6 +244,8 @@
     "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.feed_invalid_blocklist_rule": "La regla de la lista de bloqueo no es válida.",
+    "error.feed_invalid_keeplist_rule": "La regla de mantener la lista no es válida.",
     "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.",

+ 3 - 1
locale/translations/fr_FR.json

@@ -244,6 +244,8 @@
     "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.feed_invalid_blocklist_rule": "La règle de blocage n'est pas valide.",
+    "error.feed_invalid_keeplist_rule": "La règle d'autorisation n'est pas valide.",
     "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.",
@@ -263,7 +265,7 @@
     "form.feed.label.rewrite_rules": "Règles de réécriture",
     "form.feed.label.blocklist_rules": "Règles de blocage",
     "form.feed.label.keeplist_rules": "Règles d'autorisation",
-    "form.feed.label.ignore_http_cache": "Ignore cache HTTP",
+    "form.feed.label.ignore_http_cache": "Ignorer le cache HTTP",
     "form.feed.label.fetch_via_proxy": "Récupérer via proxy",
     "form.feed.label.disabled": "Ne pas actualiser ce flux",
     "form.category.label.title": "Titre",

+ 2 - 0
locale/translations/it_IT.json

@@ -244,6 +244,8 @@
     "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.feed_invalid_blocklist_rule": "La regola dell'elenco di blocco non è valida.",
+    "error.feed_invalid_keeplist_rule": "La regola dell'elenco di conservazione non è valida.",
     "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.",

+ 2 - 0
locale/translations/ja_JP.json

@@ -244,6 +244,8 @@
     "error.site_url_not_empty": "サイトのURLを空にすることはできません。",
     "error.feed_title_not_empty": "フィードのタイトルを空にすることはできません。",
     "error.feed_category_not_found": "このカテゴリは存在しないか、このユーザーに属していません。",
+    "error.feed_invalid_blocklist_rule": "ブロックリストルールが無効です。",
+    "error.feed_invalid_keeplist_rule": "リストの保持ルールが無効です。",
     "error.user_mandatory_fields": "ユーザー名が必要です。",
     "error.api_key_already_exists": "このAPIキーは既に存在します。",
     "error.unable_to_create_api_key": "このAPIキーを作成できません。",

+ 2 - 0
locale/translations/nl_NL.json

@@ -244,6 +244,8 @@
     "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.feed_invalid_blocklist_rule": "De regel voor de blokkeerlijst is ongeldig.",
+    "error.feed_invalid_keeplist_rule": "De regel voor het bewaren van een lijst is ongeldig.",
     "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.",

+ 2 - 0
locale/translations/pl_PL.json

@@ -246,6 +246,8 @@
     "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.feed_invalid_blocklist_rule": "Reguła listy zablokowanych jest nieprawidłowa.",
+    "error.feed_invalid_keeplist_rule": "Reguła listy zachowania jest nieprawidłowa.",
     "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.",

+ 2 - 0
locale/translations/pt_BR.json

@@ -244,6 +244,8 @@
     "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.feed_invalid_blocklist_rule": "A regra da lista de bloqueio é inválida.",
+    "error.feed_invalid_keeplist_rule": "A regra de manutenção da lista é inválida.",
     "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.",

+ 2 - 0
locale/translations/ru_RU.json

@@ -246,6 +246,8 @@
     "error.site_url_not_empty": "URL сайта не может быть пустым.",
     "error.feed_title_not_empty": "Заголовок фида не может быть пустым.",
     "error.feed_category_not_found": "Эта категория не существует или не принадлежит этому пользователю.",
+    "error.feed_invalid_blocklist_rule": "Правило черного списка недействительно.",
+    "error.feed_invalid_keeplist_rule": "Правило списка хранения недействительно.",
     "error.user_mandatory_fields": "Имя пользователя обязательно.",
     "error.api_key_already_exists": "Этот ключ API уже существует.",
     "error.unable_to_create_api_key": "Невозможно создать этот ключ API.",

+ 2 - 0
locale/translations/zh_CN.json

@@ -242,6 +242,8 @@
     "error.site_url_not_empty": "网站网址不能为空。",
     "error.feed_title_not_empty": "供稿标题不能为空。",
     "error.feed_category_not_found": "此类别不存在或不属于该用户。",
+    "error.feed_invalid_blocklist_rule": "阻止列表规则无效。",
+    "error.feed_invalid_keeplist_rule": "保留列表规则无效。",
     "error.user_mandatory_fields": "必须填写用户名",
     "error.api_key_already_exists": "此API密钥已存在。",
     "error.unable_to_create_api_key": "无法创建此API密钥。",

+ 49 - 0
tests/feed_test.go

@@ -202,6 +202,55 @@ func TestCreateFeedWithScraperRule(t *testing.T) {
 	}
 }
 
+func TestCreateFeedWithKeeplistRule(t *testing.T) {
+	client := createClient(t)
+
+	categories, err := client.Categories()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	feedID, err := client.CreateFeed(&miniflux.FeedCreationRequest{
+		FeedURL:       testFeedURL,
+		CategoryID:    categories[0].ID,
+		KeeplistRules: "(?i)miniflux",
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if feedID == 0 {
+		t.Fatalf(`Invalid feed ID, got %q`, feedID)
+	}
+
+	feed, err := client.Feed(feedID)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if feed.KeeplistRules != "(?i)miniflux" {
+		t.Error(`The feed should have the custom keep list rule saved`)
+	}
+}
+
+func TestCreateFeedWithInvalidBlocklistRule(t *testing.T) {
+	client := createClient(t)
+
+	categories, err := client.Categories()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = client.CreateFeed(&miniflux.FeedCreationRequest{
+		FeedURL:        testFeedURL,
+		CategoryID:     categories[0].ID,
+		BlocklistRules: "[",
+	})
+	if err == nil {
+		t.Fatal(`Feed with invalid block list rule should not be created`)
+	}
+}
+
 func TestUpdateFeedURL(t *testing.T) {
 	client := createClient(t)
 	feed, _ := createFeed(t, client)

+ 6 - 4
ui/feed_update.go

@@ -58,10 +58,12 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
 	view.Set("defaultUserAgent", config.Opts.HTTPClientUserAgent())
 
 	feedModificationRequest := &model.FeedModificationRequest{
-		FeedURL:    model.OptionalString(feedForm.FeedURL),
-		SiteURL:    model.OptionalString(feedForm.SiteURL),
-		Title:      model.OptionalString(feedForm.Title),
-		CategoryID: model.OptionalInt64(feedForm.CategoryID),
+		FeedURL:        model.OptionalString(feedForm.FeedURL),
+		SiteURL:        model.OptionalString(feedForm.SiteURL),
+		Title:          model.OptionalString(feedForm.Title),
+		CategoryID:     model.OptionalInt64(feedForm.CategoryID),
+		BlocklistRules: model.OptionalString(feedForm.BlocklistRules),
+		KeeplistRules:  model.OptionalString(feedForm.KeeplistRules),
 	}
 
 	if validationErr := validator.ValidateFeedModification(h.store, loggedUser.ID, feedModificationRequest); validationErr != nil {

+ 13 - 0
ui/form/subscription.go

@@ -9,6 +9,7 @@ import (
 	"strconv"
 
 	"miniflux.app/errors"
+	"miniflux.app/validator"
 )
 
 // SubscriptionForm represents the subscription form.
@@ -32,6 +33,18 @@ func (s *SubscriptionForm) Validate() error {
 		return errors.NewLocalizedError("error.feed_mandatory_fields")
 	}
 
+	if !validator.IsValidURL(s.URL) {
+		return errors.NewLocalizedError("error.invalid_feed_url")
+	}
+
+	if !validator.IsValidRegex(s.BlocklistRules) {
+		return errors.NewLocalizedError("error.feed_invalid_blocklist_rule")
+	}
+
+	if !validator.IsValidRegex(s.KeeplistRules) {
+		return errors.NewLocalizedError("error.feed_invalid_keeplist_rule")
+	}
+
 	return nil
 }
 

+ 23 - 3
validator/feed.go

@@ -15,7 +15,7 @@ func ValidateFeedCreation(store *storage.Storage, userID int64, request *model.F
 		return NewValidationError("error.feed_mandatory_fields")
 	}
 
-	if !isValidURL(request.FeedURL) {
+	if !IsValidURL(request.FeedURL) {
 		return NewValidationError("error.invalid_feed_url")
 	}
 
@@ -27,6 +27,14 @@ func ValidateFeedCreation(store *storage.Storage, userID int64, request *model.F
 		return NewValidationError("error.feed_category_not_found")
 	}
 
+	if !IsValidRegex(request.BlocklistRules) {
+		return NewValidationError("error.feed_invalid_blocklist_rule")
+	}
+
+	if !IsValidRegex(request.KeeplistRules) {
+		return NewValidationError("error.feed_invalid_keeplist_rule")
+	}
+
 	return nil
 }
 
@@ -37,7 +45,7 @@ func ValidateFeedModification(store *storage.Storage, userID int64, request *mod
 			return NewValidationError("error.feed_url_not_empty")
 		}
 
-		if !isValidURL(*request.FeedURL) {
+		if !IsValidURL(*request.FeedURL) {
 			return NewValidationError("error.invalid_feed_url")
 		}
 	}
@@ -47,7 +55,7 @@ func ValidateFeedModification(store *storage.Storage, userID int64, request *mod
 			return NewValidationError("error.site_url_not_empty")
 		}
 
-		if !isValidURL(*request.SiteURL) {
+		if !IsValidURL(*request.SiteURL) {
 			return NewValidationError("error.invalid_site_url")
 		}
 	}
@@ -64,5 +72,17 @@ func ValidateFeedModification(store *storage.Storage, userID int64, request *mod
 		}
 	}
 
+	if request.BlocklistRules != nil {
+		if !IsValidRegex(*request.BlocklistRules) {
+			return NewValidationError("error.feed_invalid_blocklist_rule")
+		}
+	}
+
+	if request.KeeplistRules != nil {
+		if !IsValidRegex(*request.KeeplistRules) {
+			return NewValidationError("error.feed_invalid_keeplist_rule")
+		}
+	}
+
 	return nil
 }

+ 1 - 1
validator/subscription.go

@@ -8,7 +8,7 @@ import "miniflux.app/model"
 
 // ValidateSubscriptionDiscovery validates subscription discovery requests.
 func ValidateSubscriptionDiscovery(request *model.SubscriptionDiscoveryRequest) *ValidationError {
-	if !isValidURL(request.URL) {
+	if !IsValidURL(request.URL) {
 		return NewValidationError("error.invalid_site_url")
 	}
 

+ 9 - 1
validator/validator.go

@@ -8,6 +8,7 @@ import (
 	"errors"
 	"fmt"
 	"net/url"
+	"regexp"
 
 	"miniflux.app/locale"
 )
@@ -53,7 +54,14 @@ func ValidateDirection(direction string) error {
 	return fmt.Errorf(`Invalid direction, valid direction values are: "asc" or "desc"`)
 }
 
-func isValidURL(absoluteURL string) bool {
+// IsValidRegex verifies if the regex can be compiled.
+func IsValidRegex(expr string) bool {
+	_, err := regexp.Compile(expr)
+	return err == nil
+}
+
+// IsValidURL verifies if the provided value is a valid absolute URL.
+func IsValidURL(absoluteURL string) bool {
 	_, err := url.ParseRequestURI(absoluteURL)
 	return err == nil
 }

+ 15 - 1
validator/validator_test.go

@@ -14,7 +14,7 @@ func TestIsValidURL(t *testing.T) {
 	}
 
 	for link, expected := range scenarios {
-		result := isValidURL(link)
+		result := IsValidURL(link)
 		if result != expected {
 			t.Errorf(`Unexpected result, got %v instead of %v`, result, expected)
 		}
@@ -46,3 +46,17 @@ func TestValidateDirection(t *testing.T) {
 		t.Error(`An invalid direction should generate a error`)
 	}
 }
+
+func TestIsValidRegex(t *testing.T) {
+	scenarios := map[string]bool{
+		"(?i)miniflux": true,
+		"[":            false,
+	}
+
+	for expr, expected := range scenarios {
+		result := IsValidRegex(expr)
+		if result != expected {
+			t.Errorf(`Unexpected result, got %v instead of %v`, result, expected)
+		}
+	}
+}