Add ISO-639-1 language code validation to backend (#2602)
- Add backend validation for language codes - Add migration to look for invalid ISO-639-1 language codes in workflows, crawls, and org crawling defaults, and fix any found
This commit is contained in:
parent
e17772145e
commit
1492397656
@ -46,7 +46,13 @@ from .models import (
|
||||
CrawlerProxies,
|
||||
ValidateCustomBehavior,
|
||||
)
|
||||
from .utils import dt_now, slug_from_name, validate_regexes, is_url
|
||||
from .utils import (
|
||||
dt_now,
|
||||
slug_from_name,
|
||||
validate_regexes,
|
||||
validate_language_code,
|
||||
is_url,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .orgs import OrgOps
|
||||
@ -235,6 +241,9 @@ class CrawlConfigOps:
|
||||
|
||||
self._validate_link_selectors(config_in.config.selectLinks)
|
||||
|
||||
if config_in.config.lang:
|
||||
validate_language_code(config_in.config.lang)
|
||||
|
||||
if config_in.config.customBehaviors:
|
||||
for url in config_in.config.customBehaviors:
|
||||
self._validate_custom_behavior_url_syntax(url)
|
||||
@ -406,6 +415,9 @@ class CrawlConfigOps:
|
||||
for url in update.config.customBehaviors:
|
||||
self._validate_custom_behavior_url_syntax(url)
|
||||
|
||||
if update.config and update.config.lang:
|
||||
validate_language_code(update.config.lang)
|
||||
|
||||
# indicates if any k8s crawl config settings changed
|
||||
changed = False
|
||||
changed = changed or (
|
||||
|
@ -32,7 +32,7 @@ else:
|
||||
) = PageOps = BackgroundJobOps = object
|
||||
|
||||
|
||||
CURR_DB_VERSION = "0045"
|
||||
CURR_DB_VERSION = "0046"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
265
backend/btrixcloud/migrations/migration_0046_invalid_lang.py
Normal file
265
backend/btrixcloud/migrations/migration_0046_invalid_lang.py
Normal file
@ -0,0 +1,265 @@
|
||||
"""
|
||||
Migration 0046 - Invalid language codes
|
||||
"""
|
||||
|
||||
from btrixcloud.migrations import BaseMigration
|
||||
|
||||
|
||||
MIGRATION_VERSION = "0046"
|
||||
|
||||
ISO_639_1_CODES = [
|
||||
"aa",
|
||||
"ab",
|
||||
"af",
|
||||
"ak",
|
||||
"am",
|
||||
"ar",
|
||||
"an",
|
||||
"as",
|
||||
"av",
|
||||
"ae",
|
||||
"ay",
|
||||
"az",
|
||||
"ba",
|
||||
"bm",
|
||||
"be",
|
||||
"bn",
|
||||
"bi",
|
||||
"bo",
|
||||
"bs",
|
||||
"br",
|
||||
"bg",
|
||||
"ca",
|
||||
"cs",
|
||||
"ch",
|
||||
"ce",
|
||||
"cu",
|
||||
"cv",
|
||||
"kw",
|
||||
"co",
|
||||
"cr",
|
||||
"cy",
|
||||
"da",
|
||||
"de",
|
||||
"dv",
|
||||
"dz",
|
||||
"el",
|
||||
"en",
|
||||
"eo",
|
||||
"et",
|
||||
"eu",
|
||||
"ee",
|
||||
"fo",
|
||||
"fa",
|
||||
"fj",
|
||||
"fi",
|
||||
"fr",
|
||||
"fy",
|
||||
"ff",
|
||||
"gd",
|
||||
"ga",
|
||||
"gl",
|
||||
"gv",
|
||||
"gn",
|
||||
"gu",
|
||||
"ht",
|
||||
"ha",
|
||||
"sh",
|
||||
"he",
|
||||
"hz",
|
||||
"hi",
|
||||
"ho",
|
||||
"hr",
|
||||
"hu",
|
||||
"hy",
|
||||
"ig",
|
||||
"io",
|
||||
"ii",
|
||||
"iu",
|
||||
"ie",
|
||||
"ia",
|
||||
"id",
|
||||
"ik",
|
||||
"is",
|
||||
"it",
|
||||
"jv",
|
||||
"ja",
|
||||
"kl",
|
||||
"kn",
|
||||
"ks",
|
||||
"ka",
|
||||
"kr",
|
||||
"kk",
|
||||
"km",
|
||||
"ki",
|
||||
"rw",
|
||||
"ky",
|
||||
"kv",
|
||||
"kg",
|
||||
"ko",
|
||||
"kj",
|
||||
"ku",
|
||||
"lo",
|
||||
"la",
|
||||
"lv",
|
||||
"li",
|
||||
"ln",
|
||||
"lt",
|
||||
"lb",
|
||||
"lu",
|
||||
"lg",
|
||||
"mh",
|
||||
"ml",
|
||||
"mr",
|
||||
"mk",
|
||||
"mg",
|
||||
"mt",
|
||||
"mn",
|
||||
"mi",
|
||||
"ms",
|
||||
"my",
|
||||
"na",
|
||||
"nv",
|
||||
"nr",
|
||||
"nd",
|
||||
"ng",
|
||||
"ne",
|
||||
"nl",
|
||||
"nn",
|
||||
"nb",
|
||||
"no",
|
||||
"ny",
|
||||
"oc",
|
||||
"oj",
|
||||
"or",
|
||||
"om",
|
||||
"os",
|
||||
"pa",
|
||||
"pi",
|
||||
"pl",
|
||||
"pt",
|
||||
"ps",
|
||||
"qu",
|
||||
"rm",
|
||||
"ro",
|
||||
"rn",
|
||||
"ru",
|
||||
"sg",
|
||||
"sa",
|
||||
"si",
|
||||
"sk",
|
||||
"sl",
|
||||
"se",
|
||||
"sm",
|
||||
"sn",
|
||||
"sd",
|
||||
"so",
|
||||
"st",
|
||||
"es",
|
||||
"sq",
|
||||
"sc",
|
||||
"sr",
|
||||
"ss",
|
||||
"su",
|
||||
"sw",
|
||||
"sv",
|
||||
"ty",
|
||||
"ta",
|
||||
"tt",
|
||||
"te",
|
||||
"tg",
|
||||
"tl",
|
||||
"th",
|
||||
"ti",
|
||||
"to",
|
||||
"tn",
|
||||
"ts",
|
||||
"tk",
|
||||
"tr",
|
||||
"tw",
|
||||
"ug",
|
||||
"uk",
|
||||
"ur",
|
||||
"uz",
|
||||
"ve",
|
||||
"vi",
|
||||
"vo",
|
||||
"wa",
|
||||
"wo",
|
||||
"xh",
|
||||
"yi",
|
||||
"yo",
|
||||
"za",
|
||||
"zh",
|
||||
"zu",
|
||||
]
|
||||
|
||||
|
||||
# pylint: disable=duplicate-code
|
||||
class Migration(BaseMigration):
|
||||
"""Migration class."""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def __init__(self, mdb, **kwargs):
|
||||
super().__init__(mdb, migration_version=MIGRATION_VERSION)
|
||||
|
||||
async def migrate_up(self):
|
||||
"""Perform migration up.
|
||||
|
||||
Replace any invalid ISO-639-1 language codes that may be saved in
|
||||
the database with "en".
|
||||
"""
|
||||
configs_mdb = self.mdb["crawl_configs"]
|
||||
crawls_mdb = self.mdb["crawls"]
|
||||
orgs_mdb = self.mdb["organizations"]
|
||||
|
||||
# Workflows
|
||||
try:
|
||||
result = await configs_mdb.update_many(
|
||||
{"config.lang": {"$nin": [None, *ISO_639_1_CODES]}},
|
||||
{"$set": {"config.lang": "en"}},
|
||||
)
|
||||
print(
|
||||
f"Fixed invalid language code for {result.modified_count} workflows",
|
||||
flush=True,
|
||||
)
|
||||
# pylint: disable=broad-exception-caught
|
||||
except Exception as err:
|
||||
print(
|
||||
f"Unable to update invalid language codes for crawl workflows: {err}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
# Crawls
|
||||
try:
|
||||
result = await crawls_mdb.update_many(
|
||||
{"config.lang": {"$nin": [None, *ISO_639_1_CODES]}},
|
||||
{"$set": {"config.lang": "en"}},
|
||||
)
|
||||
print(
|
||||
f"Fixed invalid language code for {result.modified_count} crawls",
|
||||
flush=True,
|
||||
)
|
||||
# pylint: disable=broad-exception-caught
|
||||
except Exception as err:
|
||||
print(
|
||||
f"Unable to update invalid language codes for crawls: {err}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
# Org crawling defaults
|
||||
try:
|
||||
result = await orgs_mdb.update_many(
|
||||
{"crawlingDefaults.lang": {"$nin": [None, *ISO_639_1_CODES]}},
|
||||
{"$set": {"crawlingDefaults.lang": "en"}},
|
||||
)
|
||||
print(
|
||||
f"Fixed invalid language code for {result.modified_count} orgs",
|
||||
flush=True,
|
||||
)
|
||||
# pylint: disable=broad-exception-caught
|
||||
except Exception as err:
|
||||
print(
|
||||
f"Unable to update invalid language codes for org crawling defaults: {err}",
|
||||
flush=True,
|
||||
)
|
@ -86,6 +86,7 @@ from .utils import (
|
||||
slug_from_name,
|
||||
validate_slug,
|
||||
get_duplicate_key_error_field,
|
||||
validate_language_code,
|
||||
JSONSerializer,
|
||||
)
|
||||
|
||||
@ -654,6 +655,9 @@ class OrgOps:
|
||||
self, org: Organization, defaults: CrawlConfigDefaults
|
||||
):
|
||||
"""Update crawling defaults"""
|
||||
if defaults.lang:
|
||||
validate_language_code(defaults.lang)
|
||||
|
||||
res = await self.orgs.find_one_and_update(
|
||||
{"_id": org.id},
|
||||
{"$set": {"crawlingDefaults": defaults.model_dump()}},
|
||||
|
@ -16,6 +16,7 @@ from uuid import UUID
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from iso639 import is_language
|
||||
from pymongo.errors import DuplicateKeyError
|
||||
from slugify import slugify
|
||||
|
||||
@ -193,3 +194,9 @@ def validate_regexes(regexes: List[str]):
|
||||
except re.error:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise HTTPException(status_code=400, detail="invalid_regex")
|
||||
|
||||
|
||||
def validate_language_code(lang: str):
|
||||
"""Validate ISO-639-1 language code, raise HTTPException if invalid"""
|
||||
if not is_language(lang, "pt1"):
|
||||
raise HTTPException(status_code=400, detail="invalid_lang")
|
||||
|
@ -28,3 +28,4 @@ types-pyYAML
|
||||
remotezip
|
||||
json-stream
|
||||
aiostream
|
||||
iso639-lang>=2.6.0
|
||||
|
@ -172,6 +172,7 @@ def test_update_config_invalid_exclude_regex(
|
||||
assert r.status_code == 400
|
||||
assert r.json()["detail"] == "invalid_regex"
|
||||
|
||||
|
||||
def test_update_config_invalid_link_selector(
|
||||
crawler_auth_headers, default_org_id, sample_crawl_data
|
||||
):
|
||||
@ -191,6 +192,20 @@ def test_update_config_invalid_link_selector(
|
||||
assert r.status_code == 400
|
||||
assert r.json()["detail"] == "invalid_link_selector"
|
||||
|
||||
|
||||
def test_update_config_invalid_lang(
|
||||
crawler_auth_headers, default_org_id, sample_crawl_data
|
||||
):
|
||||
for invalid_code in ("f", "fra", "french"):
|
||||
r = requests.patch(
|
||||
f"{API_PREFIX}/orgs/{default_org_id}/crawlconfigs/{cid}/",
|
||||
headers=crawler_auth_headers,
|
||||
json={"config": {"lang": invalid_code}},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
assert r.json()["detail"] == "invalid_lang"
|
||||
|
||||
|
||||
def test_verify_default_select_links(
|
||||
crawler_auth_headers, default_org_id, sample_crawl_data
|
||||
):
|
||||
@ -577,6 +592,20 @@ def test_add_crawl_config_invalid_exclude_regex(
|
||||
assert r.json()["detail"] == "invalid_regex"
|
||||
|
||||
|
||||
def test_add_crawl_config_invalid_lang(
|
||||
crawler_auth_headers, default_org_id, sample_crawl_data
|
||||
):
|
||||
for invalid_code in ("f", "fra", "french"):
|
||||
sample_crawl_data["config"]["lang"] = invalid_code
|
||||
r = requests.post(
|
||||
f"{API_PREFIX}/orgs/{default_org_id}/crawlconfigs/",
|
||||
headers=crawler_auth_headers,
|
||||
json=sample_crawl_data,
|
||||
)
|
||||
assert r.status_code == 400
|
||||
assert r.json()["detail"] == "invalid_lang"
|
||||
|
||||
|
||||
def test_add_crawl_config_invalid_link_selectors(
|
||||
crawler_auth_headers, default_org_id, sample_crawl_data
|
||||
):
|
||||
|
@ -81,6 +81,20 @@ def test_update_org_crawling_defaults(admin_auth_headers, default_org_id):
|
||||
]
|
||||
|
||||
|
||||
def test_update_org_crawling_defaults_invalid_lang(admin_auth_headers, default_org_id):
|
||||
for invalid_code in ("f", "fra", "french"):
|
||||
r = requests.post(
|
||||
f"{API_PREFIX}/orgs/{default_org_id}/defaults/crawling",
|
||||
headers=admin_auth_headers,
|
||||
json={
|
||||
"lang": "invalid_code",
|
||||
},
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json()["detail"] == "invalid_lang"
|
||||
|
||||
|
||||
def test_rename_org(admin_auth_headers, default_org_id):
|
||||
UPDATED_NAME = "updated org name"
|
||||
UPDATED_SLUG = "updated-org-name"
|
||||
|
@ -2340,6 +2340,9 @@ https://archiveweb.page/images/${"logo.svg"}`}
|
||||
"Page exclusion contains invalid regex",
|
||||
);
|
||||
break;
|
||||
case APIErrorDetail.InvalidLang:
|
||||
errorDetailMessage = msg("Invalid language code");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ export type APISortQuery<T = Record<string, unknown>> = {
|
||||
export enum APIErrorDetail {
|
||||
InvalidLinkSelector = "invalid_link_selector",
|
||||
InvalidRegex = "invalid_regex",
|
||||
InvalidLang = "invalid_lang",
|
||||
InvalidCustomBehavior = "invalid_custom_behavior",
|
||||
CustomBehaviorNotFound = "custom_behavior_not_found",
|
||||
CustomBehaviorBranchNotFound = "custom_behavior_branch_not_found",
|
||||
|
Loading…
Reference in New Issue
Block a user