Quellcode durchsuchen

feat(rewrite): reject invalid CSS selectors in rewrite and scraper rules

goquery returns an empty selection when a selector doesn't compile, so a
remove() or base64_decode() rule with a bad selector saves fine and then
does nothing, and bad scraper rules quietly fall back to the feed's own
content. A common way to hit this is a relative selector inside :has(),
e.g. remove("div:has(> p)"), which cascadia doesn't support.

Rewrite rules now compile-check the selector argument the same way they
already check regex arguments, and scraper rules get the same check on
feed creation, modification and the subscription form. New tests also
check that every predefined rewrite and scraper rule passes.
Dave Marquard vor 1 Woche
Ursprung
Commit
fb4f1b031e
34 geänderte Dateien mit 302 neuen und 8 gelöschten Zeilen
  1. 1 1
      go.mod
  2. 116 0
      internal/api/api_integration_test.go
  3. 2 0
      internal/locale/translations/ar_SA.json
  4. 2 0
      internal/locale/translations/de_DE.json
  5. 2 0
      internal/locale/translations/el_EL.json
  6. 2 0
      internal/locale/translations/en_US.json
  7. 2 0
      internal/locale/translations/es_ES.json
  8. 2 0
      internal/locale/translations/fi_FI.json
  9. 2 0
      internal/locale/translations/fr_FR.json
  10. 2 0
      internal/locale/translations/gl_ES.json
  11. 2 0
      internal/locale/translations/hi_IN.json
  12. 2 0
      internal/locale/translations/id_ID.json
  13. 2 0
      internal/locale/translations/it_IT.json
  14. 2 0
      internal/locale/translations/ja_JP.json
  15. 2 0
      internal/locale/translations/ko_KR.json
  16. 2 0
      internal/locale/translations/nan_Latn_pehoeji.json
  17. 2 0
      internal/locale/translations/nl_NL.json
  18. 2 0
      internal/locale/translations/pl_PL.json
  19. 2 0
      internal/locale/translations/pt_BR.json
  20. 2 0
      internal/locale/translations/ro_RO.json
  21. 2 0
      internal/locale/translations/ru_RU.json
  22. 2 0
      internal/locale/translations/tr_TR.json
  23. 2 0
      internal/locale/translations/uk_UA.json
  24. 2 0
      internal/locale/translations/zh_CN.json
  25. 2 0
      internal/locale/translations/zh_TW.json
  26. 8 7
      internal/reader/rewrite/content_rewrite.go
  27. 11 0
      internal/reader/rewrite/content_rewrite_validation.go
  28. 28 0
      internal/reader/rewrite/content_rewrite_validation_test.go
  29. 10 0
      internal/reader/scraper/scraper_test.go
  30. 4 0
      internal/ui/form/subscription.go
  31. 10 0
      internal/validator/feed.go
  32. 2 0
      internal/validator/rewrite.go
  33. 23 0
      internal/validator/scraper.go
  34. 43 0
      internal/validator/scraper_test.go

+ 1 - 1
go.mod

@@ -8,6 +8,7 @@ go 1.26.0
 require (
 require (
 	github.com/PuerkitoBio/goquery v1.13.0
 	github.com/PuerkitoBio/goquery v1.13.0
 	github.com/andybalholm/brotli v1.2.6
 	github.com/andybalholm/brotli v1.2.6
+	github.com/andybalholm/cascadia v1.3.4
 	github.com/coreos/go-oidc/v3 v3.21.0
 	github.com/coreos/go-oidc/v3 v3.21.0
 	github.com/go-webauthn/webauthn v0.18.2
 	github.com/go-webauthn/webauthn v0.18.2
 	github.com/lib/pq v1.12.3
 	github.com/lib/pq v1.12.3
@@ -28,7 +29,6 @@ require (
 )
 )
 
 
 require (
 require (
-	github.com/andybalholm/cascadia v1.3.4 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.9.4 // indirect
 	github.com/fxamacker/cbor/v2 v2.9.4 // indirect

+ 116 - 0
internal/api/api_integration_test.go

@@ -1956,6 +1956,122 @@ func TestUpdateFeedWithInvalidURLRewriteRules(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestCreateFeedWithInvalidRewriteRuleSelector(t *testing.T) {
+	testConfig := newIntegrationTestConfig()
+	if !testConfig.isConfigured() {
+		t.Skip(skipIntegrationTestsMessage)
+	}
+
+	adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
+
+	regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer adminClient.DeleteUser(regularTestUser.ID)
+
+	regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
+
+	// cascadia does not support a relative selector inside :has().
+	_, err = regularUserClient.CreateFeed(&miniflux.FeedCreationRequest{
+		FeedURL:      testConfig.testFeedURL,
+		RewriteRules: `remove("div:has(> p)")`,
+	})
+	if err == nil {
+		t.Fatalf(`Creating a feed with an invalid rewrite rule selector should raise an error`)
+	}
+}
+
+func TestUpdateFeedWithInvalidRewriteRuleSelector(t *testing.T) {
+	testConfig := newIntegrationTestConfig()
+	if !testConfig.isConfigured() {
+		t.Skip(skipIntegrationTestsMessage)
+	}
+
+	adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
+
+	regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer adminClient.DeleteUser(regularTestUser.ID)
+
+	regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
+
+	feedID, err := regularUserClient.CreateFeed(&miniflux.FeedCreationRequest{
+		FeedURL: testConfig.testFeedURL,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	feedUpdateRequest := &miniflux.FeedModificationRequest{
+		RewriteRules: new(`remove("div:has(> p)")`),
+	}
+
+	if _, err := regularUserClient.UpdateFeed(feedID, feedUpdateRequest); err == nil {
+		t.Fatalf(`Updating a feed with an invalid rewrite rule selector should raise an error`)
+	}
+}
+
+func TestCreateFeedWithInvalidScraperRules(t *testing.T) {
+	testConfig := newIntegrationTestConfig()
+	if !testConfig.isConfigured() {
+		t.Skip(skipIntegrationTestsMessage)
+	}
+
+	adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
+
+	regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer adminClient.DeleteUser(regularTestUser.ID)
+
+	regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
+
+	// cascadia does not support a relative selector inside :has().
+	_, err = regularUserClient.CreateFeed(&miniflux.FeedCreationRequest{
+		FeedURL:      testConfig.testFeedURL,
+		ScraperRules: `div:has(> p)`,
+	})
+	if err == nil {
+		t.Fatalf(`Creating a feed with invalid scraper rules should raise an error`)
+	}
+}
+
+func TestUpdateFeedWithInvalidScraperRules(t *testing.T) {
+	testConfig := newIntegrationTestConfig()
+	if !testConfig.isConfigured() {
+		t.Skip(skipIntegrationTestsMessage)
+	}
+
+	adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
+
+	regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer adminClient.DeleteUser(regularTestUser.ID)
+
+	regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
+
+	feedID, err := regularUserClient.CreateFeed(&miniflux.FeedCreationRequest{
+		FeedURL: testConfig.testFeedURL,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	feedUpdateRequest := &miniflux.FeedModificationRequest{
+		ScraperRules: new(`div:has(> p)`),
+	}
+
+	if _, err := regularUserClient.UpdateFeed(feedID, feedUpdateRequest); err == nil {
+		t.Fatalf(`Updating a feed with invalid scraper rules should raise an error`)
+	}
+}
+
 func TestMarkFeedAsReadEndpoint(t *testing.T) {
 func TestMarkFeedAsReadEndpoint(t *testing.T) {
 	t.Parallel()
 	t.Parallel()
 
 

+ 2 - 0
internal/locale/translations/ar_SA.json

@@ -116,9 +116,11 @@
     "error.feed_mandatory_fields": "الرابط والفئة إلزاميان.",
     "error.feed_mandatory_fields": "الرابط والفئة إلزاميان.",
     "error.feed_not_found": "هذا المصدر غير موجود أو لا ينتمي لهذا المستخدم.",
     "error.feed_not_found": "هذا المصدر غير موجود أو لا ينتمي لهذا المستخدم.",
     "error.feed_invalid_rewrite_rule": "قاعدة إعادة الكتابة غير صالحة.",
     "error.feed_invalid_rewrite_rule": "قاعدة إعادة الكتابة غير صالحة.",
+    "error.feed_invalid_scraper_rule": "قاعدة الكاشط ليست محدد CSS صالحًا: %s.",
     "error.feed_invalid_urlrewrite_rule": "قاعدة إعادة كتابة عنوان URL غير صالحة.",
     "error.feed_invalid_urlrewrite_rule": "قاعدة إعادة كتابة عنوان URL غير صالحة.",
     "error.feed_rewrite_rule_invalid_syntax": "بناء جملة غير صالح لقاعدة إعادة الكتابة بالقرب من العمود %d: %s (استخدم \\\\ لشرطة مائلة عكسية حرفية).",
     "error.feed_rewrite_rule_invalid_syntax": "بناء جملة غير صالح لقاعدة إعادة الكتابة بالقرب من العمود %d: %s (استخدم \\\\ لشرطة مائلة عكسية حرفية).",
     "error.feed_rewrite_rule_invalid_regex": "تحتوي قاعدة إعادة الكتابة '%s' على تعبير نمطي غير صالح: %s.",
     "error.feed_rewrite_rule_invalid_regex": "تحتوي قاعدة إعادة الكتابة '%s' على تعبير نمطي غير صالح: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "تحتوي قاعدة إعادة الكتابة '%s' على محدد CSS غير صالح: %s.",
     "error.feed_rewrite_rule_missing_args": "قاعدة إعادة الكتابة '%s' تفتقر إلى وسائط مطلوبة.",
     "error.feed_rewrite_rule_missing_args": "قاعدة إعادة الكتابة '%s' تفتقر إلى وسائط مطلوبة.",
     "error.feed_rewrite_rule_unknown_name": "قاعدة إعادة كتابة غير معروفة: '%s'.",
     "error.feed_rewrite_rule_unknown_name": "قاعدة إعادة كتابة غير معروفة: '%s'.",
     "error.feed_title_not_empty": "عنوان المصدر لا يمكن أن يكون فارغاً.",
     "error.feed_title_not_empty": "عنوان المصدر لا يمكن أن يكون فارغاً.",

+ 2 - 0
internal/locale/translations/de_DE.json

@@ -102,9 +102,11 @@
     "error.feed_mandatory_fields": "Die URL und die Kategorie sind obligatorisch.",
     "error.feed_mandatory_fields": "Die URL und die Kategorie sind obligatorisch.",
     "error.feed_not_found": "Dieses Abonnement existiert nicht oder gehört nicht zu diesem Benutzer.",
     "error.feed_not_found": "Dieses Abonnement existiert nicht oder gehört nicht zu diesem Benutzer.",
     "error.feed_invalid_rewrite_rule": "Die Rewrite-Regel ist ungültig.",
     "error.feed_invalid_rewrite_rule": "Die Rewrite-Regel ist ungültig.",
+    "error.feed_invalid_scraper_rule": "Die Extraktionsregel ist kein gültiger CSS-Selektor: %s.",
     "error.feed_invalid_urlrewrite_rule": "Die URL-Rewrite-Regel ist ungültig.",
     "error.feed_invalid_urlrewrite_rule": "Die URL-Rewrite-Regel ist ungültig.",
     "error.feed_rewrite_rule_invalid_syntax": "Ungültige Syntax der Rewrite-Regel bei Spalte %d: %s (verwenden Sie \\\\ für einen wörtlichen Backslash).",
     "error.feed_rewrite_rule_invalid_syntax": "Ungültige Syntax der Rewrite-Regel bei Spalte %d: %s (verwenden Sie \\\\ für einen wörtlichen Backslash).",
     "error.feed_rewrite_rule_invalid_regex": "Die Rewrite-Regel '%s' enthält einen ungültigen regulären Ausdruck: %s.",
     "error.feed_rewrite_rule_invalid_regex": "Die Rewrite-Regel '%s' enthält einen ungültigen regulären Ausdruck: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "Die Rewrite-Regel '%s' enthält einen ungültigen CSS-Selektor: %s.",
     "error.feed_rewrite_rule_missing_args": "Der Rewrite-Regel „%s“ fehlen erforderliche Argumente.",
     "error.feed_rewrite_rule_missing_args": "Der Rewrite-Regel „%s“ fehlen erforderliche Argumente.",
     "error.feed_rewrite_rule_unknown_name": "Unbekannte Rewrite-Regel: „%s“.",
     "error.feed_rewrite_rule_unknown_name": "Unbekannte Rewrite-Regel: „%s“.",
     "error.feed_title_not_empty": "Der Feed-Titel darf nicht leer sein.",
     "error.feed_title_not_empty": "Der Feed-Titel darf nicht leer sein.",

+ 2 - 0
internal/locale/translations/el_EL.json

@@ -102,9 +102,11 @@
     "error.feed_mandatory_fields": "Η διεύθυνση URL και η κατηγορία είναι υποχρεωτικά.",
     "error.feed_mandatory_fields": "Η διεύθυνση URL και η κατηγορία είναι υποχρεωτικά.",
     "error.feed_not_found": "Αυτή η ροή δεν υπάρχει ή δεν ανήκει σε αυτόν τον χρήστη.",
     "error.feed_not_found": "Αυτή η ροή δεν υπάρχει ή δεν ανήκει σε αυτόν τον χρήστη.",
     "error.feed_invalid_rewrite_rule": "Ο κανόνας επανεγγραφής δεν είναι έγκυρος.",
     "error.feed_invalid_rewrite_rule": "Ο κανόνας επανεγγραφής δεν είναι έγκυρος.",
+    "error.feed_invalid_scraper_rule": "Ο κανόνας Scraper δεν είναι έγκυρος επιλογέας CSS: %s.",
     "error.feed_invalid_urlrewrite_rule": "Ο κανόνας επανεγγραφής URL δεν είναι έγκυρος.",
     "error.feed_invalid_urlrewrite_rule": "Ο κανόνας επανεγγραφής URL δεν είναι έγκυρος.",
     "error.feed_rewrite_rule_invalid_syntax": "Μη έγκυρη σύνταξη κανόνα επανεγγραφής κοντά στη στήλη %d: %s (χρησιμοποιήστε \\\\ για κυριολεκτική ανάποδη κάθετο).",
     "error.feed_rewrite_rule_invalid_syntax": "Μη έγκυρη σύνταξη κανόνα επανεγγραφής κοντά στη στήλη %d: %s (χρησιμοποιήστε \\\\ για κυριολεκτική ανάποδη κάθετο).",
     "error.feed_rewrite_rule_invalid_regex": "Ο κανόνας επανεγγραφής '%s' έχει μη έγκυρη κανονική έκφραση: %s.",
     "error.feed_rewrite_rule_invalid_regex": "Ο κανόνας επανεγγραφής '%s' έχει μη έγκυρη κανονική έκφραση: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "Ο κανόνας επανεγγραφής '%s' έχει μη έγκυρο επιλογέα CSS: %s.",
     "error.feed_rewrite_rule_missing_args": "Στον κανόνα επανεγγραφής '%s' λείπουν απαιτούμενα ορίσματα.",
     "error.feed_rewrite_rule_missing_args": "Στον κανόνα επανεγγραφής '%s' λείπουν απαιτούμενα ορίσματα.",
     "error.feed_rewrite_rule_unknown_name": "Άγνωστος κανόνας επανεγγραφής: '%s'.",
     "error.feed_rewrite_rule_unknown_name": "Άγνωστος κανόνας επανεγγραφής: '%s'.",
     "error.feed_title_not_empty": "Ο τίτλος ροής δεν μπορεί να είναι κενός.",
     "error.feed_title_not_empty": "Ο τίτλος ροής δεν μπορεί να είναι κενός.",

+ 2 - 0
internal/locale/translations/en_US.json

@@ -102,11 +102,13 @@
     "error.feed_invalid_blocklist_rule": "The block list rule is invalid.",
     "error.feed_invalid_blocklist_rule": "The block list rule is invalid.",
     "error.feed_invalid_keeplist_rule": "The keep list rule is invalid.",
     "error.feed_invalid_keeplist_rule": "The keep list rule is invalid.",
     "error.feed_invalid_rewrite_rule": "The rewrite rule is invalid.",
     "error.feed_invalid_rewrite_rule": "The rewrite rule is invalid.",
+    "error.feed_invalid_scraper_rule": "The scraper rule is not a valid CSS selector: %s.",
     "error.feed_invalid_urlrewrite_rule": "The URL rewrite rule is invalid (expected format: rewrite(\"search\"|\"replacement\")).",
     "error.feed_invalid_urlrewrite_rule": "The URL rewrite rule is invalid (expected format: rewrite(\"search\"|\"replacement\")).",
     "error.feed_mandatory_fields": "The URL and the category are mandatory.",
     "error.feed_mandatory_fields": "The URL and the category are mandatory.",
     "error.feed_not_found": "This feed does not exist or does not belong to this user.",
     "error.feed_not_found": "This feed does not exist or does not belong to this user.",
     "error.feed_rewrite_rule_invalid_syntax": "Invalid rewrite rule syntax near column %d: %s (use \\\\ for a literal backslash).",
     "error.feed_rewrite_rule_invalid_syntax": "Invalid rewrite rule syntax near column %d: %s (use \\\\ for a literal backslash).",
     "error.feed_rewrite_rule_invalid_regex": "Rewrite rule '%s' has an invalid regular expression: %s.",
     "error.feed_rewrite_rule_invalid_regex": "Rewrite rule '%s' has an invalid regular expression: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "Rewrite rule '%s' has an invalid CSS selector: %s.",
     "error.feed_rewrite_rule_missing_args": "Rewrite rule '%s' is missing required arguments.",
     "error.feed_rewrite_rule_missing_args": "Rewrite rule '%s' is missing required arguments.",
     "error.feed_rewrite_rule_unknown_name": "Unknown rewrite rule: '%s'.",
     "error.feed_rewrite_rule_unknown_name": "Unknown rewrite rule: '%s'.",
     "error.feed_title_not_empty": "The feed title cannot be empty.",
     "error.feed_title_not_empty": "The feed title cannot be empty.",

+ 2 - 0
internal/locale/translations/es_ES.json

@@ -102,9 +102,11 @@
     "error.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.",
     "error.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.",
     "error.feed_not_found": "Este feed no existe o no pertenece a este usuario.",
     "error.feed_not_found": "Este feed no existe o no pertenece a este usuario.",
     "error.feed_invalid_rewrite_rule": "La regla de reescritura no es válida.",
     "error.feed_invalid_rewrite_rule": "La regla de reescritura no es válida.",
+    "error.feed_invalid_scraper_rule": "La regla de extracción no es un selector CSS válido: %s.",
     "error.feed_invalid_urlrewrite_rule": "La regla de reescritura de URL no es válida.",
     "error.feed_invalid_urlrewrite_rule": "La regla de reescritura de URL no es válida.",
     "error.feed_rewrite_rule_invalid_syntax": "Sintaxis de regla de reescritura no válida cerca de la columna %d: %s (use \\\\ para una barra invertida literal).",
     "error.feed_rewrite_rule_invalid_syntax": "Sintaxis de regla de reescritura no válida cerca de la columna %d: %s (use \\\\ para una barra invertida literal).",
     "error.feed_rewrite_rule_invalid_regex": "La regla de reescritura '%s' tiene una expresión regular no válida: %s.",
     "error.feed_rewrite_rule_invalid_regex": "La regla de reescritura '%s' tiene una expresión regular no válida: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "La regla de reescritura '%s' tiene un selector CSS no válido: %s.",
     "error.feed_rewrite_rule_missing_args": "A la regla de reescritura '%s' le faltan argumentos obligatorios.",
     "error.feed_rewrite_rule_missing_args": "A la regla de reescritura '%s' le faltan argumentos obligatorios.",
     "error.feed_rewrite_rule_unknown_name": "Regla de reescritura desconocida: '%s'.",
     "error.feed_rewrite_rule_unknown_name": "Regla de reescritura desconocida: '%s'.",
     "error.feed_title_not_empty": "El título del feed no puede estar vacío.",
     "error.feed_title_not_empty": "El título del feed no puede estar vacío.",

+ 2 - 0
internal/locale/translations/fi_FI.json

@@ -102,9 +102,11 @@
     "error.feed_mandatory_fields": "URL-osoite ja kategoria ovat pakollisia.",
     "error.feed_mandatory_fields": "URL-osoite ja kategoria ovat pakollisia.",
     "error.feed_not_found": "Tämä syöte ei ole olemassa tai se ei kuulu tälle käyttäjälle.",
     "error.feed_not_found": "Tämä syöte ei ole olemassa tai se ei kuulu tälle käyttäjälle.",
     "error.feed_invalid_rewrite_rule": "Uudelleenkirjoitussääntö on virheellinen.",
     "error.feed_invalid_rewrite_rule": "Uudelleenkirjoitussääntö on virheellinen.",
+    "error.feed_invalid_scraper_rule": "Scraper-sääntö ei ole kelvollinen CSS-valitsin: %s.",
     "error.feed_invalid_urlrewrite_rule": "URL-uudelleenkirjoitussääntö on virheellinen.",
     "error.feed_invalid_urlrewrite_rule": "URL-uudelleenkirjoitussääntö on virheellinen.",
     "error.feed_rewrite_rule_invalid_syntax": "Virheellinen uudelleenkirjoitussäännön syntaksi sarakkeen %d lähellä: %s (käytä \\\\ kirjaimellista kenoviivaa varten).",
     "error.feed_rewrite_rule_invalid_syntax": "Virheellinen uudelleenkirjoitussäännön syntaksi sarakkeen %d lähellä: %s (käytä \\\\ kirjaimellista kenoviivaa varten).",
     "error.feed_rewrite_rule_invalid_regex": "Uudelleenkirjoitussäännöllä '%s' on virheellinen säännöllinen lauseke: %s.",
     "error.feed_rewrite_rule_invalid_regex": "Uudelleenkirjoitussäännöllä '%s' on virheellinen säännöllinen lauseke: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "Uudelleenkirjoitussäännöllä '%s' on virheellinen CSS-valitsin: %s.",
     "error.feed_rewrite_rule_missing_args": "Uudelleenkirjoitussäännöstä '%s' puuttuu vaadittuja argumentteja.",
     "error.feed_rewrite_rule_missing_args": "Uudelleenkirjoitussäännöstä '%s' puuttuu vaadittuja argumentteja.",
     "error.feed_rewrite_rule_unknown_name": "Tuntematon uudelleenkirjoitussääntö: '%s'.",
     "error.feed_rewrite_rule_unknown_name": "Tuntematon uudelleenkirjoitussääntö: '%s'.",
     "error.feed_title_not_empty": "Syötteen otsikko ei voi olla tyhjä.",
     "error.feed_title_not_empty": "Syötteen otsikko ei voi olla tyhjä.",

+ 2 - 0
internal/locale/translations/fr_FR.json

@@ -102,9 +102,11 @@
     "error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.",
     "error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.",
     "error.feed_not_found": "Impossible de trouver ce flux.",
     "error.feed_not_found": "Impossible de trouver ce flux.",
     "error.feed_invalid_rewrite_rule": "La règle de réécriture n'est pas valide.",
     "error.feed_invalid_rewrite_rule": "La règle de réécriture n'est pas valide.",
+    "error.feed_invalid_scraper_rule": "La règle de récupération du contenu n'est pas un sélecteur CSS valide : %s.",
     "error.feed_invalid_urlrewrite_rule": "La règle de réécriture d'URL n'est pas valide.",
     "error.feed_invalid_urlrewrite_rule": "La règle de réécriture d'URL n'est pas valide.",
     "error.feed_rewrite_rule_invalid_syntax": "Syntaxe de règle de réécriture invalide près de la colonne %d : %s (utilisez \\\\ pour une barre oblique inverse littérale).",
     "error.feed_rewrite_rule_invalid_syntax": "Syntaxe de règle de réécriture invalide près de la colonne %d : %s (utilisez \\\\ pour une barre oblique inverse littérale).",
     "error.feed_rewrite_rule_invalid_regex": "La règle de réécriture '%s' contient une expression régulière invalide : %s.",
     "error.feed_rewrite_rule_invalid_regex": "La règle de réécriture '%s' contient une expression régulière invalide : %s.",
+    "error.feed_rewrite_rule_invalid_selector": "La règle de réécriture '%s' contient un sélecteur CSS invalide : %s.",
     "error.feed_rewrite_rule_missing_args": "Il manque des arguments requis à la règle de réécriture « %s ».",
     "error.feed_rewrite_rule_missing_args": "Il manque des arguments requis à la règle de réécriture « %s ».",
     "error.feed_rewrite_rule_unknown_name": "Règle de réécriture inconnue : « %s ».",
     "error.feed_rewrite_rule_unknown_name": "Règle de réécriture inconnue : « %s ».",
     "error.feed_title_not_empty": "Le titre du flux ne peut pas être vide.",
     "error.feed_title_not_empty": "Le titre du flux ne peut pas être vide.",

+ 2 - 0
internal/locale/translations/gl_ES.json

@@ -104,9 +104,11 @@
     "error.feed_mandatory_fields": "É obrigatorio engadir un URL para a categoría.",
     "error.feed_mandatory_fields": "É obrigatorio engadir un URL para a categoría.",
     "error.feed_not_found": "A canle non existe ou non pertence a esta usuaria.",
     "error.feed_not_found": "A canle non existe ou non pertence a esta usuaria.",
     "error.feed_invalid_rewrite_rule": "A regra de reescritura non é válida.",
     "error.feed_invalid_rewrite_rule": "A regra de reescritura non é válida.",
+    "error.feed_invalid_scraper_rule": "A regra para obter contido non é un selector CSS válido: %s.",
     "error.feed_invalid_urlrewrite_rule": "A regra de reescritura de URL non é válida.",
     "error.feed_invalid_urlrewrite_rule": "A regra de reescritura de URL non é válida.",
     "error.feed_rewrite_rule_invalid_syntax": "Sintaxe de regra de reescritura non válida preto da columna %d: %s (usa \\\\ para unha barra invertida literal).",
     "error.feed_rewrite_rule_invalid_syntax": "Sintaxe de regra de reescritura non válida preto da columna %d: %s (usa \\\\ para unha barra invertida literal).",
     "error.feed_rewrite_rule_invalid_regex": "A regra de reescritura '%s' ten unha expresión regular non válida: %s.",
     "error.feed_rewrite_rule_invalid_regex": "A regra de reescritura '%s' ten unha expresión regular non válida: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "A regra de reescritura '%s' ten un selector CSS non válido: %s.",
     "error.feed_rewrite_rule_missing_args": "Á regra de reescritura '%s' fáltanlle argumentos obrigatorios.",
     "error.feed_rewrite_rule_missing_args": "Á regra de reescritura '%s' fáltanlle argumentos obrigatorios.",
     "error.feed_rewrite_rule_unknown_name": "Regra de reescritura descoñecida: '%s'.",
     "error.feed_rewrite_rule_unknown_name": "Regra de reescritura descoñecida: '%s'.",
     "error.feed_title_not_empty": "O título da canle non pode quedar baleiro.",
     "error.feed_title_not_empty": "O título da canle non pode quedar baleiro.",

+ 2 - 0
internal/locale/translations/hi_IN.json

@@ -102,9 +102,11 @@
     "error.feed_mandatory_fields": "URL और श्रेणी अनिवार्य हैं।",
     "error.feed_mandatory_fields": "URL और श्रेणी अनिवार्य हैं।",
     "error.feed_not_found": "यह फ़ीड मौजूद नहीं है या इस उपयोगकर्ता से संबंधित नहीं है।",
     "error.feed_not_found": "यह फ़ीड मौजूद नहीं है या इस उपयोगकर्ता से संबंधित नहीं है।",
     "error.feed_invalid_rewrite_rule": "रीराइट नियम अमान्य है।",
     "error.feed_invalid_rewrite_rule": "रीराइट नियम अमान्य है।",
+    "error.feed_invalid_scraper_rule": "खुरचनी नियम एक मान्य CSS सेलेक्टर नहीं है: %s।",
     "error.feed_invalid_urlrewrite_rule": "URL रीराइट नियम अमान्य है।",
     "error.feed_invalid_urlrewrite_rule": "URL रीराइट नियम अमान्य है।",
     "error.feed_rewrite_rule_invalid_syntax": "कॉलम %d के पास अमान्य रीराइट नियम सिंटैक्स: %s (शाब्दिक बैकस्लैश के लिए \\\\ का उपयोग करें)।",
     "error.feed_rewrite_rule_invalid_syntax": "कॉलम %d के पास अमान्य रीराइट नियम सिंटैक्स: %s (शाब्दिक बैकस्लैश के लिए \\\\ का उपयोग करें)।",
     "error.feed_rewrite_rule_invalid_regex": "रीराइट नियम '%s' में अमान्य रेगुलर एक्सप्रेशन है: %s।",
     "error.feed_rewrite_rule_invalid_regex": "रीराइट नियम '%s' में अमान्य रेगुलर एक्सप्रेशन है: %s।",
+    "error.feed_rewrite_rule_invalid_selector": "रीराइट नियम '%s' में अमान्य CSS सेलेक्टर है: %s।",
     "error.feed_rewrite_rule_missing_args": "रीराइट नियम '%s' में आवश्यक तर्क अनुपस्थित हैं।",
     "error.feed_rewrite_rule_missing_args": "रीराइट नियम '%s' में आवश्यक तर्क अनुपस्थित हैं।",
     "error.feed_rewrite_rule_unknown_name": "अज्ञात रीराइट नियम: '%s'।",
     "error.feed_rewrite_rule_unknown_name": "अज्ञात रीराइट नियम: '%s'।",
     "error.feed_title_not_empty": "फ़ीड शीर्षक खाली नहीं हो सकता.",
     "error.feed_title_not_empty": "फ़ीड शीर्षक खाली नहीं हो सकता.",

+ 2 - 0
internal/locale/translations/id_ID.json

@@ -99,9 +99,11 @@
     "error.feed_mandatory_fields": "Harus ada URL dan kategorinya.",
     "error.feed_mandatory_fields": "Harus ada URL dan kategorinya.",
     "error.feed_not_found": "Umpan ini tidak ada atau tidak dipunyai oleh pengguna ini",
     "error.feed_not_found": "Umpan ini tidak ada atau tidak dipunyai oleh pengguna ini",
     "error.feed_invalid_rewrite_rule": "Aturan penulisan ulang tidak valid.",
     "error.feed_invalid_rewrite_rule": "Aturan penulisan ulang tidak valid.",
+    "error.feed_invalid_scraper_rule": "Aturan pengambil data bukan selektor CSS yang valid: %s.",
     "error.feed_invalid_urlrewrite_rule": "Aturan penulisan ulang URL tidak valid.",
     "error.feed_invalid_urlrewrite_rule": "Aturan penulisan ulang URL tidak valid.",
     "error.feed_rewrite_rule_invalid_syntax": "Sintaks aturan penulisan ulang tidak valid di dekat kolom %d: %s (gunakan \\\\ untuk garis miring terbalik literal).",
     "error.feed_rewrite_rule_invalid_syntax": "Sintaks aturan penulisan ulang tidak valid di dekat kolom %d: %s (gunakan \\\\ untuk garis miring terbalik literal).",
     "error.feed_rewrite_rule_invalid_regex": "Aturan penulisan ulang '%s' memiliki ekspresi reguler yang tidak valid: %s.",
     "error.feed_rewrite_rule_invalid_regex": "Aturan penulisan ulang '%s' memiliki ekspresi reguler yang tidak valid: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "Aturan penulisan ulang '%s' memiliki selektor CSS yang tidak valid: %s.",
     "error.feed_rewrite_rule_missing_args": "Aturan penulisan ulang '%s' tidak memiliki argumen yang diperlukan.",
     "error.feed_rewrite_rule_missing_args": "Aturan penulisan ulang '%s' tidak memiliki argumen yang diperlukan.",
     "error.feed_rewrite_rule_unknown_name": "Aturan penulisan ulang tidak dikenal: '%s'.",
     "error.feed_rewrite_rule_unknown_name": "Aturan penulisan ulang tidak dikenal: '%s'.",
     "error.feed_title_not_empty": "Judul umpan tidak boleh kosong.",
     "error.feed_title_not_empty": "Judul umpan tidak boleh kosong.",

+ 2 - 0
internal/locale/translations/it_IT.json

@@ -102,9 +102,11 @@
     "error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.",
     "error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.",
     "error.feed_not_found": "Questo feed non esiste o non appartiene a questo utente.",
     "error.feed_not_found": "Questo feed non esiste o non appartiene a questo utente.",
     "error.feed_invalid_rewrite_rule": "La regola di riscrittura non è valida.",
     "error.feed_invalid_rewrite_rule": "La regola di riscrittura non è valida.",
+    "error.feed_invalid_scraper_rule": "La regola di estrazione del contenuto non è un selettore CSS valido: %s.",
     "error.feed_invalid_urlrewrite_rule": "La regola di riscrittura dell'URL non è valida.",
     "error.feed_invalid_urlrewrite_rule": "La regola di riscrittura dell'URL non è valida.",
     "error.feed_rewrite_rule_invalid_syntax": "Sintassi della regola di riscrittura non valida vicino alla colonna %d: %s (usa \\\\ per una barra rovesciata letterale).",
     "error.feed_rewrite_rule_invalid_syntax": "Sintassi della regola di riscrittura non valida vicino alla colonna %d: %s (usa \\\\ per una barra rovesciata letterale).",
     "error.feed_rewrite_rule_invalid_regex": "La regola di riscrittura '%s' contiene un'espressione regolare non valida: %s.",
     "error.feed_rewrite_rule_invalid_regex": "La regola di riscrittura '%s' contiene un'espressione regolare non valida: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "La regola di riscrittura '%s' contiene un selettore CSS non valido: %s.",
     "error.feed_rewrite_rule_missing_args": "Alla regola di riscrittura '%s' mancano gli argomenti richiesti.",
     "error.feed_rewrite_rule_missing_args": "Alla regola di riscrittura '%s' mancano gli argomenti richiesti.",
     "error.feed_rewrite_rule_unknown_name": "Regola di riscrittura sconosciuta: '%s'.",
     "error.feed_rewrite_rule_unknown_name": "Regola di riscrittura sconosciuta: '%s'.",
     "error.feed_title_not_empty": "Il titolo del feed non può essere vuoto.",
     "error.feed_title_not_empty": "Il titolo del feed non può essere vuoto.",

+ 2 - 0
internal/locale/translations/ja_JP.json

@@ -99,9 +99,11 @@
     "error.feed_mandatory_fields": "URL と カテゴリが必要です。",
     "error.feed_mandatory_fields": "URL と カテゴリが必要です。",
     "error.feed_not_found": "このフィードは存在しないか、このユーザーに属していません。",
     "error.feed_not_found": "このフィードは存在しないか、このユーザーに属していません。",
     "error.feed_invalid_rewrite_rule": "書き換えルールが無効です。",
     "error.feed_invalid_rewrite_rule": "書き換えルールが無効です。",
+    "error.feed_invalid_scraper_rule": "Scraper ルールが有効な CSS セレクターではありません: %s。",
     "error.feed_invalid_urlrewrite_rule": "URL書き換えルールが無効です。",
     "error.feed_invalid_urlrewrite_rule": "URL書き換えルールが無効です。",
     "error.feed_rewrite_rule_invalid_syntax": "列 %d 付近の書き換えルールの構文が無効です: %s(リテラルのバックスラッシュには \\\\ を使用してください)。",
     "error.feed_rewrite_rule_invalid_syntax": "列 %d 付近の書き換えルールの構文が無効です: %s(リテラルのバックスラッシュには \\\\ を使用してください)。",
     "error.feed_rewrite_rule_invalid_regex": "書き換えルール '%s' の正規表現が無効です: %s。",
     "error.feed_rewrite_rule_invalid_regex": "書き換えルール '%s' の正規表現が無効です: %s。",
+    "error.feed_rewrite_rule_invalid_selector": "書き換えルール '%s' の CSS セレクターが無効です: %s。",
     "error.feed_rewrite_rule_missing_args": "書き換えルール「%s」に必要な引数がありません。",
     "error.feed_rewrite_rule_missing_args": "書き換えルール「%s」に必要な引数がありません。",
     "error.feed_rewrite_rule_unknown_name": "不明な書き換えルール: 「%s」。",
     "error.feed_rewrite_rule_unknown_name": "不明な書き換えルール: 「%s」。",
     "error.feed_title_not_empty": "フィードのタイトルを空にすることはできません。",
     "error.feed_title_not_empty": "フィードのタイトルを空にすることはできません。",

+ 2 - 0
internal/locale/translations/ko_KR.json

@@ -99,9 +99,11 @@
     "error.feed_mandatory_fields": "URL과 카테고리가 필요합니다.",
     "error.feed_mandatory_fields": "URL과 카테고리가 필요합니다.",
     "error.feed_not_found": "이 피드는 존재하지 않거나 이 사용자의 것이 아닙니다.",
     "error.feed_not_found": "이 피드는 존재하지 않거나 이 사용자의 것이 아닙니다.",
     "error.feed_invalid_rewrite_rule": "다시 쓰기 규칙이 잘못되었습니다.",
     "error.feed_invalid_rewrite_rule": "다시 쓰기 규칙이 잘못되었습니다.",
+    "error.feed_invalid_scraper_rule": "본문 추출 규칙이 올바른 CSS 선택자가 아닙니다: %s.",
     "error.feed_invalid_urlrewrite_rule": "URL 다시 쓰기 규칙이 잘못되었습니다.",
     "error.feed_invalid_urlrewrite_rule": "URL 다시 쓰기 규칙이 잘못되었습니다.",
     "error.feed_rewrite_rule_invalid_syntax": "%d열 부근의 다시 쓰기 규칙 구문이 잘못되었습니다: %s (리터럴 백슬래시는 \\\\ 를 사용하세요).",
     "error.feed_rewrite_rule_invalid_syntax": "%d열 부근의 다시 쓰기 규칙 구문이 잘못되었습니다: %s (리터럴 백슬래시는 \\\\ 를 사용하세요).",
     "error.feed_rewrite_rule_invalid_regex": "다시 쓰기 규칙 '%s'의 정규 표현식이 잘못되었습니다: %s.",
     "error.feed_rewrite_rule_invalid_regex": "다시 쓰기 규칙 '%s'의 정규 표현식이 잘못되었습니다: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "다시 쓰기 규칙 '%s'의 CSS 선택자가 잘못되었습니다: %s.",
     "error.feed_rewrite_rule_missing_args": "다시 쓰기 규칙 '%s'에 필요한 인수가 없습니다.",
     "error.feed_rewrite_rule_missing_args": "다시 쓰기 규칙 '%s'에 필요한 인수가 없습니다.",
     "error.feed_rewrite_rule_unknown_name": "알 수 없는 다시 쓰기 규칙: '%s'.",
     "error.feed_rewrite_rule_unknown_name": "알 수 없는 다시 쓰기 규칙: '%s'.",
     "error.feed_title_not_empty": "피드 제목은 비워 둘 수 없습니다.",
     "error.feed_title_not_empty": "피드 제목은 비워 둘 수 없습니다.",

+ 2 - 0
internal/locale/translations/nan_Latn_pehoeji.json

@@ -99,9 +99,11 @@
     "error.feed_mandatory_fields": "Tio̍h-ài su-lip bāng-chí kah lūi-pia̍t.",
     "error.feed_mandatory_fields": "Tio̍h-ài su-lip bāng-chí kah lūi-pia̍t.",
     "error.feed_not_found": "Chhē bô chit ê siau-sit lâi-goân ah-sī bô sio̍k-tī lí",
     "error.feed_not_found": "Chhē bô chit ê siau-sit lâi-goân ah-sī bô sio̍k-tī lí",
     "error.feed_invalid_rewrite_rule": "Tiông-siá kui-chek bô-hāu.",
     "error.feed_invalid_rewrite_rule": "Tiông-siá kui-chek bô-hāu.",
+    "error.feed_invalid_scraper_rule": "Lia̍h ê kui-chek m̄-sī ū-hāu ê CSS soán-te̍k-khì: %s.",
     "error.feed_invalid_urlrewrite_rule": "URL tiông-siá kui-chek bô-hāu.",
     "error.feed_invalid_urlrewrite_rule": "URL tiông-siá kui-chek bô-hāu.",
     "error.feed_rewrite_rule_invalid_syntax": "Tī %d chōa hù-kīn ê tiông-siá kui-chek gí-hoat bô-hāu: %s (iōng \\\\ lâi piáu-sī jī-bīn ê péng-siâ-sòaⁿ).",
     "error.feed_rewrite_rule_invalid_syntax": "Tī %d chōa hù-kīn ê tiông-siá kui-chek gí-hoat bô-hāu: %s (iōng \\\\ lâi piáu-sī jī-bīn ê péng-siâ-sòaⁿ).",
     "error.feed_rewrite_rule_invalid_regex": "Tiông-siá kui-chek '%s' ê chèng-kui piáu-ta̍t-sek bô-hāu: %s.",
     "error.feed_rewrite_rule_invalid_regex": "Tiông-siá kui-chek '%s' ê chèng-kui piáu-ta̍t-sek bô-hāu: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "Tiông-siá kui-chek '%s' ê CSS soán-te̍k-khì bô-hāu: %s.",
     "error.feed_rewrite_rule_missing_args": "Tiông-siá kui-chek '%s' khiàm su-iàu ê chham-sò·.",
     "error.feed_rewrite_rule_missing_args": "Tiông-siá kui-chek '%s' khiàm su-iàu ê chham-sò·.",
     "error.feed_rewrite_rule_unknown_name": "Bô se-sek ê tiông-siá kui-chek: '%s'.",
     "error.feed_rewrite_rule_unknown_name": "Bô se-sek ê tiông-siá kui-chek: '%s'.",
     "error.feed_title_not_empty": "Beh tēng ê siau-sit lâi-goân ê piau-tôe bōe-sái sī khang--ê.",
     "error.feed_title_not_empty": "Beh tēng ê siau-sit lâi-goân ê piau-tôe bōe-sái sī khang--ê.",

+ 2 - 0
internal/locale/translations/nl_NL.json

@@ -102,9 +102,11 @@
     "error.feed_mandatory_fields": "De velden URL en categorie zijn verplicht.",
     "error.feed_mandatory_fields": "De velden URL en categorie zijn verplicht.",
     "error.feed_not_found": "Deze feed bestaat niet of is niet van deze gebruiker.",
     "error.feed_not_found": "Deze feed bestaat niet of is niet van deze gebruiker.",
     "error.feed_invalid_rewrite_rule": "De herschrijfregel is ongeldig.",
     "error.feed_invalid_rewrite_rule": "De herschrijfregel is ongeldig.",
+    "error.feed_invalid_scraper_rule": "De extractieregel is geen geldige CSS-selector: %s.",
     "error.feed_invalid_urlrewrite_rule": "De URL-herschrijfregel is ongeldig.",
     "error.feed_invalid_urlrewrite_rule": "De URL-herschrijfregel is ongeldig.",
     "error.feed_rewrite_rule_invalid_syntax": "Ongeldige syntaxis van herschrijfregel nabij kolom %d: %s (gebruik \\\\ voor een letterlijke backslash).",
     "error.feed_rewrite_rule_invalid_syntax": "Ongeldige syntaxis van herschrijfregel nabij kolom %d: %s (gebruik \\\\ voor een letterlijke backslash).",
     "error.feed_rewrite_rule_invalid_regex": "Herschrijfregel '%s' heeft een ongeldige reguliere expressie: %s.",
     "error.feed_rewrite_rule_invalid_regex": "Herschrijfregel '%s' heeft een ongeldige reguliere expressie: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "Herschrijfregel '%s' heeft een ongeldige CSS-selector: %s.",
     "error.feed_rewrite_rule_missing_args": "Herschrijfregel '%s' mist vereiste argumenten.",
     "error.feed_rewrite_rule_missing_args": "Herschrijfregel '%s' mist vereiste argumenten.",
     "error.feed_rewrite_rule_unknown_name": "Onbekende herschrijfregel: '%s'.",
     "error.feed_rewrite_rule_unknown_name": "Onbekende herschrijfregel: '%s'.",
     "error.feed_title_not_empty": "De feed titel mag niet leeg zijn.",
     "error.feed_title_not_empty": "De feed titel mag niet leeg zijn.",

+ 2 - 0
internal/locale/translations/pl_PL.json

@@ -105,9 +105,11 @@
     "error.feed_mandatory_fields": "Adres URL i kategoria są obowiązkowe.",
     "error.feed_mandatory_fields": "Adres URL i kategoria są obowiązkowe.",
     "error.feed_not_found": "Ten kanał nie istnieje lub nie należy do tego użytkownika.",
     "error.feed_not_found": "Ten kanał nie istnieje lub nie należy do tego użytkownika.",
     "error.feed_invalid_rewrite_rule": "Reguła przepisywania jest nieprawidłowa.",
     "error.feed_invalid_rewrite_rule": "Reguła przepisywania jest nieprawidłowa.",
+    "error.feed_invalid_scraper_rule": "Reguła ekstrakcji nie jest prawidłowym selektorem CSS: %s.",
     "error.feed_invalid_urlrewrite_rule": "Reguła przepisywania adresu URL jest nieprawidłowa.",
     "error.feed_invalid_urlrewrite_rule": "Reguła przepisywania adresu URL jest nieprawidłowa.",
     "error.feed_rewrite_rule_invalid_syntax": "Nieprawidłowa składnia reguły przepisywania w pobliżu kolumny %d: %s (użyj \\\\ dla dosłownego ukośnika wstecznego).",
     "error.feed_rewrite_rule_invalid_syntax": "Nieprawidłowa składnia reguły przepisywania w pobliżu kolumny %d: %s (użyj \\\\ dla dosłownego ukośnika wstecznego).",
     "error.feed_rewrite_rule_invalid_regex": "Reguła przepisywania '%s' ma nieprawidłowe wyrażenie regularne: %s.",
     "error.feed_rewrite_rule_invalid_regex": "Reguła przepisywania '%s' ma nieprawidłowe wyrażenie regularne: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "Reguła przepisywania '%s' ma nieprawidłowy selektor CSS: %s.",
     "error.feed_rewrite_rule_missing_args": "W regule przepisywania „%s” brakuje wymaganych argumentów.",
     "error.feed_rewrite_rule_missing_args": "W regule przepisywania „%s” brakuje wymaganych argumentów.",
     "error.feed_rewrite_rule_unknown_name": "Nieznana reguła przepisywania: „%s”.",
     "error.feed_rewrite_rule_unknown_name": "Nieznana reguła przepisywania: „%s”.",
     "error.feed_title_not_empty": "Tytuł kanału nie może być pusty.",
     "error.feed_title_not_empty": "Tytuł kanału nie może być pusty.",

+ 2 - 0
internal/locale/translations/pt_BR.json

@@ -102,9 +102,11 @@
     "error.feed_mandatory_fields": "O campo de URL e categoria são obrigatórios.",
     "error.feed_mandatory_fields": "O campo de URL e categoria são obrigatórios.",
     "error.feed_not_found": "Esta fonte não existe ou não pertence a este usuário.",
     "error.feed_not_found": "Esta fonte não existe ou não pertence a este usuário.",
     "error.feed_invalid_rewrite_rule": "A regra de reescrita é inválida.",
     "error.feed_invalid_rewrite_rule": "A regra de reescrita é inválida.",
+    "error.feed_invalid_scraper_rule": "A regra do scraper não é um seletor CSS válido: %s.",
     "error.feed_invalid_urlrewrite_rule": "A regra de reescrita de URL é inválida.",
     "error.feed_invalid_urlrewrite_rule": "A regra de reescrita de URL é inválida.",
     "error.feed_rewrite_rule_invalid_syntax": "Sintaxe de regra de reescrita inválida próximo à coluna %d: %s (use \\\\ para uma barra invertida literal).",
     "error.feed_rewrite_rule_invalid_syntax": "Sintaxe de regra de reescrita inválida próximo à coluna %d: %s (use \\\\ para uma barra invertida literal).",
     "error.feed_rewrite_rule_invalid_regex": "A regra de reescrita '%s' tem uma expressão regular inválida: %s.",
     "error.feed_rewrite_rule_invalid_regex": "A regra de reescrita '%s' tem uma expressão regular inválida: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "A regra de reescrita '%s' tem um seletor CSS inválido: %s.",
     "error.feed_rewrite_rule_missing_args": "A regra de reescrita '%s' está sem argumentos obrigatórios.",
     "error.feed_rewrite_rule_missing_args": "A regra de reescrita '%s' está sem argumentos obrigatórios.",
     "error.feed_rewrite_rule_unknown_name": "Regra de reescrita desconhecida: '%s'.",
     "error.feed_rewrite_rule_unknown_name": "Regra de reescrita desconhecida: '%s'.",
     "error.feed_title_not_empty": "O título do feed não pode estar vazio.",
     "error.feed_title_not_empty": "O título do feed não pode estar vazio.",

+ 2 - 0
internal/locale/translations/ro_RO.json

@@ -105,9 +105,11 @@
     "error.feed_mandatory_fields": "Adresa URL și categoria sunt obligatorii.",
     "error.feed_mandatory_fields": "Adresa URL și categoria sunt obligatorii.",
     "error.feed_not_found": "Acest flux nu există sau un aparține acestui utilizator.",
     "error.feed_not_found": "Acest flux nu există sau un aparține acestui utilizator.",
     "error.feed_invalid_rewrite_rule": "Regula de rescriere nu este validă.",
     "error.feed_invalid_rewrite_rule": "Regula de rescriere nu este validă.",
+    "error.feed_invalid_scraper_rule": "Regula de eliminare nu este un selector CSS valid: %s.",
     "error.feed_invalid_urlrewrite_rule": "Regula de rescriere a URL-ului nu este validă.",
     "error.feed_invalid_urlrewrite_rule": "Regula de rescriere a URL-ului nu este validă.",
     "error.feed_rewrite_rule_invalid_syntax": "Sintaxă de regulă de rescriere nevalidă lângă coloana %d: %s (utilizați \\\\ pentru o bară oblică inversă literală).",
     "error.feed_rewrite_rule_invalid_syntax": "Sintaxă de regulă de rescriere nevalidă lângă coloana %d: %s (utilizați \\\\ pentru o bară oblică inversă literală).",
     "error.feed_rewrite_rule_invalid_regex": "Regula de rescriere '%s' are o expresie regulată nevalidă: %s.",
     "error.feed_rewrite_rule_invalid_regex": "Regula de rescriere '%s' are o expresie regulată nevalidă: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "Regula de rescriere '%s' are un selector CSS nevalid: %s.",
     "error.feed_rewrite_rule_missing_args": "Regulii de rescriere '%s' îi lipsesc argumente obligatorii.",
     "error.feed_rewrite_rule_missing_args": "Regulii de rescriere '%s' îi lipsesc argumente obligatorii.",
     "error.feed_rewrite_rule_unknown_name": "Regulă de rescriere necunoscută: '%s'.",
     "error.feed_rewrite_rule_unknown_name": "Regulă de rescriere necunoscută: '%s'.",
     "error.feed_title_not_empty": "Titlul fluxului nu poate fi gol.",
     "error.feed_title_not_empty": "Titlul fluxului nu poate fi gol.",

+ 2 - 0
internal/locale/translations/ru_RU.json

@@ -105,9 +105,11 @@
     "error.feed_mandatory_fields": "Ссылка и категория обязательны.",
     "error.feed_mandatory_fields": "Ссылка и категория обязательны.",
     "error.feed_not_found": "Эта подписка не существует или не принадлежит этому пользователю.",
     "error.feed_not_found": "Эта подписка не существует или не принадлежит этому пользователю.",
     "error.feed_invalid_rewrite_rule": "Правило перезаписи недействительно.",
     "error.feed_invalid_rewrite_rule": "Правило перезаписи недействительно.",
+    "error.feed_invalid_scraper_rule": "Правило сборщика не является допустимым CSS-селектором: %s.",
     "error.feed_invalid_urlrewrite_rule": "Правило перезаписи URL недействительно.",
     "error.feed_invalid_urlrewrite_rule": "Правило перезаписи URL недействительно.",
     "error.feed_rewrite_rule_invalid_syntax": "Недопустимый синтаксис правила перезаписи рядом со столбцом %d: %s (используйте \\\\ для обратной косой черты).",
     "error.feed_rewrite_rule_invalid_syntax": "Недопустимый синтаксис правила перезаписи рядом со столбцом %d: %s (используйте \\\\ для обратной косой черты).",
     "error.feed_rewrite_rule_invalid_regex": "Правило перезаписи '%s' содержит недопустимое регулярное выражение: %s.",
     "error.feed_rewrite_rule_invalid_regex": "Правило перезаписи '%s' содержит недопустимое регулярное выражение: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "Правило перезаписи '%s' содержит недопустимый CSS-селектор: %s.",
     "error.feed_rewrite_rule_missing_args": "В правиле перезаписи «%s» отсутствуют обязательные аргументы.",
     "error.feed_rewrite_rule_missing_args": "В правиле перезаписи «%s» отсутствуют обязательные аргументы.",
     "error.feed_rewrite_rule_unknown_name": "Неизвестное правило перезаписи: «%s».",
     "error.feed_rewrite_rule_unknown_name": "Неизвестное правило перезаписи: «%s».",
     "error.feed_title_not_empty": "Заголовок подписки не может быть пустым.",
     "error.feed_title_not_empty": "Заголовок подписки не может быть пустым.",

+ 2 - 0
internal/locale/translations/tr_TR.json

@@ -102,9 +102,11 @@
     "error.feed_mandatory_fields": "URL ve kategori zorunlu.",
     "error.feed_mandatory_fields": "URL ve kategori zorunlu.",
     "error.feed_not_found": "Bu makele mevcut değil ya da bu kullanıcıya ait değil.",
     "error.feed_not_found": "Bu makele mevcut değil ya da bu kullanıcıya ait değil.",
     "error.feed_invalid_rewrite_rule": "Yeniden yazma kuralı geçersiz.",
     "error.feed_invalid_rewrite_rule": "Yeniden yazma kuralı geçersiz.",
+    "error.feed_invalid_scraper_rule": "Scrapper kuralı geçerli bir CSS seçicisi değil: %s.",
     "error.feed_invalid_urlrewrite_rule": "URL yeniden yazma kuralı geçersiz.",
     "error.feed_invalid_urlrewrite_rule": "URL yeniden yazma kuralı geçersiz.",
     "error.feed_rewrite_rule_invalid_syntax": "%d. sütun yakınında geçersiz yeniden yazma kuralı sözdizimi: %s (gerçek ters eğik çizgi için \\\\ kullanın).",
     "error.feed_rewrite_rule_invalid_syntax": "%d. sütun yakınında geçersiz yeniden yazma kuralı sözdizimi: %s (gerçek ters eğik çizgi için \\\\ kullanın).",
     "error.feed_rewrite_rule_invalid_regex": "Yeniden yazma kuralı '%s' geçersiz bir düzenli ifadeye sahip: %s.",
     "error.feed_rewrite_rule_invalid_regex": "Yeniden yazma kuralı '%s' geçersiz bir düzenli ifadeye sahip: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "Yeniden yazma kuralı '%s' geçersiz bir CSS seçicisine sahip: %s.",
     "error.feed_rewrite_rule_missing_args": "'%s' yeniden yazma kuralında gerekli argümanlar eksik.",
     "error.feed_rewrite_rule_missing_args": "'%s' yeniden yazma kuralında gerekli argümanlar eksik.",
     "error.feed_rewrite_rule_unknown_name": "Bilinmeyen yeniden yazma kuralı: '%s'.",
     "error.feed_rewrite_rule_unknown_name": "Bilinmeyen yeniden yazma kuralı: '%s'.",
     "error.feed_title_not_empty": "Besleme başlığı boş olamaz.",
     "error.feed_title_not_empty": "Besleme başlığı boş olamaz.",

+ 2 - 0
internal/locale/translations/uk_UA.json

@@ -105,9 +105,11 @@
     "error.feed_mandatory_fields": "URL та категорія є обов’язковими.",
     "error.feed_mandatory_fields": "URL та категорія є обов’язковими.",
     "error.feed_not_found": "Ця стрічка не існує або не належить цьому користувачу.",
     "error.feed_not_found": "Ця стрічка не існує або не належить цьому користувачу.",
     "error.feed_invalid_rewrite_rule": "Правило перезапису недійсне.",
     "error.feed_invalid_rewrite_rule": "Правило перезапису недійсне.",
+    "error.feed_invalid_scraper_rule": "Правило Scraper не є дійсним CSS-селектором: %s.",
     "error.feed_invalid_urlrewrite_rule": "Правило перезапису URL недійсне.",
     "error.feed_invalid_urlrewrite_rule": "Правило перезапису URL недійсне.",
     "error.feed_rewrite_rule_invalid_syntax": "Некоректний синтаксис правила перезапису біля стовпця %d: %s (використовуйте \\\\ для зворотної косої риски).",
     "error.feed_rewrite_rule_invalid_syntax": "Некоректний синтаксис правила перезапису біля стовпця %d: %s (використовуйте \\\\ для зворотної косої риски).",
     "error.feed_rewrite_rule_invalid_regex": "Правило перезапису '%s' містить недійсний регулярний вираз: %s.",
     "error.feed_rewrite_rule_invalid_regex": "Правило перезапису '%s' містить недійсний регулярний вираз: %s.",
+    "error.feed_rewrite_rule_invalid_selector": "Правило перезапису '%s' містить недійсний CSS-селектор: %s.",
     "error.feed_rewrite_rule_missing_args": "У правилі перезапису «%s» відсутні обов'язкові аргументи.",
     "error.feed_rewrite_rule_missing_args": "У правилі перезапису «%s» відсутні обов'язкові аргументи.",
     "error.feed_rewrite_rule_unknown_name": "Невідоме правило перезапису: «%s».",
     "error.feed_rewrite_rule_unknown_name": "Невідоме правило перезапису: «%s».",
     "error.feed_title_not_empty": "Назва стрічки не може бути порожньою.",
     "error.feed_title_not_empty": "Назва стрічки не може бути порожньою.",

+ 2 - 0
internal/locale/translations/zh_CN.json

@@ -99,9 +99,11 @@
     "error.feed_mandatory_fields": "必须填写 URL 和分类。",
     "error.feed_mandatory_fields": "必须填写 URL 和分类。",
     "error.feed_not_found": "此订阅源不存在或不属于此用户。",
     "error.feed_not_found": "此订阅源不存在或不属于此用户。",
     "error.feed_invalid_rewrite_rule": "重写规则无效。",
     "error.feed_invalid_rewrite_rule": "重写规则无效。",
+    "error.feed_invalid_scraper_rule": "抓取规则不是有效的 CSS 选择器:%s。",
     "error.feed_invalid_urlrewrite_rule": "URL 重写规则无效。",
     "error.feed_invalid_urlrewrite_rule": "URL 重写规则无效。",
     "error.feed_rewrite_rule_invalid_syntax": "第 %d 列附近的重写规则语法无效:%s(使用 \\\\ 表示字面反斜杠)。",
     "error.feed_rewrite_rule_invalid_syntax": "第 %d 列附近的重写规则语法无效:%s(使用 \\\\ 表示字面反斜杠)。",
     "error.feed_rewrite_rule_invalid_regex": "重写规则 '%s' 的正则表达式无效:%s。",
     "error.feed_rewrite_rule_invalid_regex": "重写规则 '%s' 的正则表达式无效:%s。",
+    "error.feed_rewrite_rule_invalid_selector": "重写规则 '%s' 的 CSS 选择器无效:%s。",
     "error.feed_rewrite_rule_missing_args": "重写规则“%s”缺少必需的参数。",
     "error.feed_rewrite_rule_missing_args": "重写规则“%s”缺少必需的参数。",
     "error.feed_rewrite_rule_unknown_name": "未知的重写规则:“%s”。",
     "error.feed_rewrite_rule_unknown_name": "未知的重写规则:“%s”。",
     "error.feed_title_not_empty": "订阅源的标题不能为空。",
     "error.feed_title_not_empty": "订阅源的标题不能为空。",

+ 2 - 0
internal/locale/translations/zh_TW.json

@@ -99,9 +99,11 @@
     "error.feed_mandatory_fields": "必須填寫網址和分類",
     "error.feed_mandatory_fields": "必須填寫網址和分類",
     "error.feed_not_found": "無法找到此 Feed 或不屬於您。",
     "error.feed_not_found": "無法找到此 Feed 或不屬於您。",
     "error.feed_invalid_rewrite_rule": "重寫規則無效。",
     "error.feed_invalid_rewrite_rule": "重寫規則無效。",
+    "error.feed_invalid_scraper_rule": "抓取規則不是有效的 CSS 選擇器:%s。",
     "error.feed_invalid_urlrewrite_rule": "URL 重寫規則無效。",
     "error.feed_invalid_urlrewrite_rule": "URL 重寫規則無效。",
     "error.feed_rewrite_rule_invalid_syntax": "第 %d 欄附近的重寫規則語法無效:%s(使用 \\\\ 表示字面反斜線)。",
     "error.feed_rewrite_rule_invalid_syntax": "第 %d 欄附近的重寫規則語法無效:%s(使用 \\\\ 表示字面反斜線)。",
     "error.feed_rewrite_rule_invalid_regex": "重寫規則 '%s' 的正規表達式無效:%s。",
     "error.feed_rewrite_rule_invalid_regex": "重寫規則 '%s' 的正規表達式無效:%s。",
+    "error.feed_rewrite_rule_invalid_selector": "重寫規則 '%s' 的 CSS 選擇器無效:%s。",
     "error.feed_rewrite_rule_missing_args": "重寫規則「%s」缺少必要的參數。",
     "error.feed_rewrite_rule_missing_args": "重寫規則「%s」缺少必要的參數。",
     "error.feed_rewrite_rule_unknown_name": "未知的重寫規則:「%s」。",
     "error.feed_rewrite_rule_unknown_name": "未知的重寫規則:「%s」。",
     "error.feed_title_not_empty": "訂閱的標題不能為空。",
     "error.feed_title_not_empty": "訂閱的標題不能為空。",

+ 8 - 7
internal/reader/rewrite/content_rewrite.go

@@ -20,12 +20,13 @@ type rule struct {
 
 
 // ruleSpec describes a rewrite rule: the minimum number of arguments it requires
 // ruleSpec describes a rewrite rule: the minimum number of arguments it requires
 // (used for save-time validation), whether its first argument is a regular
 // (used for save-time validation), whether its first argument is a regular
-// expression (so it can be compile-checked at save time), and the handler that
-// applies it.
+// expression or a CSS selector (so it can be compile-checked at save time), and
+// the handler that applies it.
 type ruleSpec struct {
 type ruleSpec struct {
-	minArgs  int
-	regexArg bool
-	apply    func(entryURL string, entry *model.Entry, args []string)
+	minArgs     int
+	regexArg    bool
+	selectorArg bool
+	apply       func(entryURL string, entry *model.Entry, args []string)
 }
 }
 
 
 // convertTextLinksRule is shared by the "convert_text_link" and
 // convertTextLinksRule is shared by the "convert_text_link" and
@@ -84,7 +85,7 @@ var knownRules = map[string]ruleSpec{
 		// Format: replace_title("search-term"|"replace-term")
 		// Format: replace_title("search-term"|"replace-term")
 		entry.Title = replaceCustom(entry.Title, args[0], args[1])
 		entry.Title = replaceCustom(entry.Title, args[0], args[1])
 	}},
 	}},
-	"remove": {minArgs: 1, apply: func(_ string, entry *model.Entry, args []string) {
+	"remove": {minArgs: 1, selectorArg: true, apply: func(_ string, entry *model.Entry, args []string) {
 		// Format: remove("#selector > .element, .another")
 		// Format: remove("#selector > .element, .another")
 		entry.Content = removeCustom(entry.Content, args[0])
 		entry.Content = removeCustom(entry.Content, args[0])
 	}},
 	}},
@@ -94,7 +95,7 @@ var knownRules = map[string]ruleSpec{
 	"add_castopod_episode": {apply: func(entryURL string, entry *model.Entry, _ []string) {
 	"add_castopod_episode": {apply: func(entryURL string, entry *model.Entry, _ []string) {
 		entry.Content = addCastopodEpisode(entryURL, entry.Content)
 		entry.Content = addCastopodEpisode(entryURL, entry.Content)
 	}},
 	}},
-	"base64_decode": {apply: func(_ string, entry *model.Entry, args []string) {
+	"base64_decode": {selectorArg: true, apply: func(_ string, entry *model.Entry, args []string) {
 		selector := "body"
 		selector := "body"
 		if len(args) >= 1 {
 		if len(args) >= 1 {
 			selector = args[0]
 			selector = args[0]

+ 11 - 0
internal/reader/rewrite/content_rewrite_validation.go

@@ -6,6 +6,8 @@ package rewrite // import "miniflux.app/v2/internal/reader/rewrite"
 import (
 import (
 	"regexp"
 	"regexp"
 	"text/scanner"
 	"text/scanner"
+
+	"github.com/andybalholm/cascadia"
 )
 )
 
 
 type RuleErrorKind int
 type RuleErrorKind int
@@ -17,6 +19,7 @@ const (
 	RuleErrUnknownName
 	RuleErrUnknownName
 	RuleErrMissingArgs
 	RuleErrMissingArgs
 	RuleErrInvalidRegexp
 	RuleErrInvalidRegexp
+	RuleErrInvalidSelector
 )
 )
 
 
 // RuleError describes one problem found while parsing rewrite rules.
 // RuleError describes one problem found while parsing rewrite rules.
@@ -49,6 +52,14 @@ func validateParsedRules(rules []rule) (errs []RuleError) {
 					Token: r.args[0], Message: err.Error()})
 					Token: r.args[0], Message: err.Error()})
 			}
 			}
 		}
 		}
+		// goquery silently matches nothing when a selector does not compile,
+		// so an invalid selector would turn the rule into a no-op.
+		if spec.selectorArg && len(r.args) > 0 {
+			if _, err := cascadia.Compile(r.args[0]); err != nil {
+				errs = append(errs, RuleError{Kind: RuleErrInvalidSelector, Rule: r.name,
+					Token: r.args[0], Message: err.Error()})
+			}
+		}
 	}
 	}
 	return errs
 	return errs
 }
 }

+ 28 - 0
internal/reader/rewrite/content_rewrite_validation_test.go

@@ -71,6 +71,34 @@ func TestParseRulesInvalidRegexp(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestParseRulesInvalidSelector(t *testing.T) {
+	for _, rulesText := range []string{
+		`remove("div:has(> p)")`,
+		`remove(".a,,.b")`,
+		`base64_decode("div[")`,
+	} {
+		_, errs := parseRules(rulesText)
+		if len(errs) != 1 || errs[0].Kind != RuleErrInvalidSelector {
+			t.Errorf(`Expected one RuleErrInvalidSelector error for %s, got %v`, rulesText, errs)
+		}
+	}
+}
+
+func TestParseRulesValidSelector(t *testing.T) {
+	rulesText := `remove(".newsletter, div.share > a, p:contains(Subscribe)") base64_decode base64_decode("pre")`
+	if _, errs := parseRules(rulesText); len(errs) != 0 {
+		t.Errorf(`Expected zero errors, got %v`, errs)
+	}
+}
+
+func TestPredefinedRulesAreValid(t *testing.T) {
+	for domain, rulesText := range predefinedRules {
+		if errs := ValidateRules(rulesText); len(errs) != 0 {
+			t.Errorf(`Predefined rewrite rules for %s are invalid: %v`, domain, errs)
+		}
+	}
+}
+
 func TestParseRulesWellFormed(t *testing.T) {
 func TestParseRulesWellFormed(t *testing.T) {
 	rulesText := `replace("a"|"b") nl2br`
 	rulesText := `replace("a"|"b") nl2br`
 	rules, errs := parseRules(rulesText)
 	rules, errs := parseRules(rulesText)

+ 10 - 0
internal/reader/scraper/scraper_test.go

@@ -8,8 +8,18 @@ import (
 	"os"
 	"os"
 	"strings"
 	"strings"
 	"testing"
 	"testing"
+
+	"github.com/andybalholm/cascadia"
 )
 )
 
 
+func TestPredefinedRulesAreValidSelectors(t *testing.T) {
+	for domain, rules := range predefinedRules {
+		if _, err := cascadia.Compile(rules); err != nil {
+			t.Errorf("Predefined scraper rules for %s are not a valid CSS selector: %v", domain, err)
+		}
+	}
+}
+
 func TestGetPredefinedRules(t *testing.T) {
 func TestGetPredefinedRules(t *testing.T) {
 	if getPredefinedScraperRules("http://www.phoronix.com/") == "" {
 	if getPredefinedScraperRules("http://www.phoronix.com/") == "" {
 		t.Error("Unable to find rule for phoronix.com")
 		t.Error("Unable to find rule for phoronix.com")

+ 4 - 0
internal/ui/form/subscription.go

@@ -61,6 +61,10 @@ func (s *SubscriptionForm) Validate() *locale.LocalizedError {
 		return err
 		return err
 	}
 	}
 
 
+	if err := validator.IsValidScraperRules(s.ScraperRules); err != nil {
+		return err
+	}
+
 	if s.ProxyURL != "" && !urllib.IsValidProxyURL(s.ProxyURL) {
 	if s.ProxyURL != "" && !urllib.IsValidProxyURL(s.ProxyURL) {
 		return locale.NewLocalizedError("error.invalid_feed_proxy_url")
 		return locale.NewLocalizedError("error.invalid_feed_proxy_url")
 	}
 	}

+ 10 - 0
internal/validator/feed.go

@@ -74,6 +74,10 @@ func ValidateFeedCreation(store *storage.Storage, userID int64, request *model.F
 		return err
 		return err
 	}
 	}
 
 
+	if err := IsValidScraperRules(request.ScraperRules); err != nil {
+		return err
+	}
+
 	if request.ProxyURL != "" && !urllib.IsValidProxyURL(request.ProxyURL) {
 	if request.ProxyURL != "" && !urllib.IsValidProxyURL(request.ProxyURL) {
 		return locale.NewLocalizedError("error.invalid_feed_proxy_url")
 		return locale.NewLocalizedError("error.invalid_feed_proxy_url")
 	}
 	}
@@ -147,6 +151,12 @@ func ValidateFeedModification(store *storage.Storage, userID, feedID int64, requ
 		}
 		}
 	}
 	}
 
 
+	if request.ScraperRules != nil {
+		if err := IsValidScraperRules(*request.ScraperRules); err != nil {
+			return err
+		}
+	}
+
 	if request.UrlRewriteRules != nil {
 	if request.UrlRewriteRules != nil {
 		if !IsValidURLRewriteRules(*request.UrlRewriteRules) {
 		if !IsValidURLRewriteRules(*request.UrlRewriteRules) {
 			return locale.NewLocalizedError("error.feed_invalid_urlrewrite_rule")
 			return locale.NewLocalizedError("error.feed_invalid_urlrewrite_rule")

+ 2 - 0
internal/validator/rewrite.go

@@ -34,6 +34,8 @@ func IsValidRewriteRules(rules string) *locale.LocalizedError {
 		return locale.NewLocalizedError("error.feed_rewrite_rule_missing_args", e.Rule)
 		return locale.NewLocalizedError("error.feed_rewrite_rule_missing_args", e.Rule)
 	case rewrite.RuleErrInvalidRegexp:
 	case rewrite.RuleErrInvalidRegexp:
 		return locale.NewLocalizedError("error.feed_rewrite_rule_invalid_regex", e.Rule, e.Message)
 		return locale.NewLocalizedError("error.feed_rewrite_rule_invalid_regex", e.Rule, e.Message)
+	case rewrite.RuleErrInvalidSelector:
+		return locale.NewLocalizedError("error.feed_rewrite_rule_invalid_selector", e.Rule, e.Message)
 	case rewrite.RuleErrUnquote, rewrite.RuleErrLexical:
 	case rewrite.RuleErrUnquote, rewrite.RuleErrLexical:
 		// Lexical (scanner) errors carry a descriptive Message but no token,
 		// Lexical (scanner) errors carry a descriptive Message but no token,
 		// while unquote errors carry the offending token; show whichever we have.
 		// while unquote errors carry the offending token; show whichever we have.

+ 23 - 0
internal/validator/scraper.go

@@ -0,0 +1,23 @@
+// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package validator // import "miniflux.app/v2/internal/validator"
+
+import (
+	"github.com/andybalholm/cascadia"
+
+	"miniflux.app/v2/internal/locale"
+)
+
+// IsValidScraperRules validates scraper rules, which are a CSS selector. The
+// scraper silently extracts nothing when the selector does not compile, so an
+// invalid one is rejected at save time instead.
+func IsValidScraperRules(rules string) *locale.LocalizedError {
+	if rules == "" {
+		return nil
+	}
+	if _, err := cascadia.Compile(rules); err != nil {
+		return locale.NewLocalizedError("error.feed_invalid_scraper_rule", err.Error())
+	}
+	return nil
+}

+ 43 - 0
internal/validator/scraper_test.go

@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package validator // import "miniflux.app/v2/internal/validator"
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestIsValidScraperRules(t *testing.T) {
+	tests := []struct {
+		rules   string
+		wantErr bool
+	}{
+		{"", false},
+		{"article", false},
+		{"div.content, .post-body > p", false},
+		{"div:has(> p)", true},
+		{"div[", true},
+		{"p:first", true},
+	}
+
+	for _, tc := range tests {
+		err := IsValidScraperRules(tc.rules)
+		if (err != nil) != tc.wantErr {
+			t.Errorf("IsValidScraperRules(%q): expected error %v, got %v", tc.rules, tc.wantErr, err)
+		}
+		if err != nil && !strings.Contains(err.String(), "not a valid CSS selector") {
+			t.Errorf("IsValidScraperRules(%q): unexpected error message: %q", tc.rules, err.String())
+		}
+	}
+}
+
+func TestIsValidRewriteRulesInvalidSelector(t *testing.T) {
+	err := IsValidRewriteRules(`remove("div:has(> p)")`)
+	if err == nil {
+		t.Fatal("expected an error for an invalid selector")
+	}
+	if msg := err.String(); !strings.Contains(msg, "invalid CSS selector") {
+		t.Errorf("unexpected error message: %q", msg)
+	}
+}