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,
|
CrawlerProxies,
|
||||||
ValidateCustomBehavior,
|
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:
|
if TYPE_CHECKING:
|
||||||
from .orgs import OrgOps
|
from .orgs import OrgOps
|
||||||
@ -235,6 +241,9 @@ class CrawlConfigOps:
|
|||||||
|
|
||||||
self._validate_link_selectors(config_in.config.selectLinks)
|
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:
|
if config_in.config.customBehaviors:
|
||||||
for url in config_in.config.customBehaviors:
|
for url in config_in.config.customBehaviors:
|
||||||
self._validate_custom_behavior_url_syntax(url)
|
self._validate_custom_behavior_url_syntax(url)
|
||||||
@ -406,6 +415,9 @@ class CrawlConfigOps:
|
|||||||
for url in update.config.customBehaviors:
|
for url in update.config.customBehaviors:
|
||||||
self._validate_custom_behavior_url_syntax(url)
|
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
|
# indicates if any k8s crawl config settings changed
|
||||||
changed = False
|
changed = False
|
||||||
changed = changed or (
|
changed = changed or (
|
||||||
|
@ -32,7 +32,7 @@ else:
|
|||||||
) = PageOps = BackgroundJobOps = object
|
) = 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,
|
slug_from_name,
|
||||||
validate_slug,
|
validate_slug,
|
||||||
get_duplicate_key_error_field,
|
get_duplicate_key_error_field,
|
||||||
|
validate_language_code,
|
||||||
JSONSerializer,
|
JSONSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -654,6 +655,9 @@ class OrgOps:
|
|||||||
self, org: Organization, defaults: CrawlConfigDefaults
|
self, org: Organization, defaults: CrawlConfigDefaults
|
||||||
):
|
):
|
||||||
"""Update crawling defaults"""
|
"""Update crawling defaults"""
|
||||||
|
if defaults.lang:
|
||||||
|
validate_language_code(defaults.lang)
|
||||||
|
|
||||||
res = await self.orgs.find_one_and_update(
|
res = await self.orgs.find_one_and_update(
|
||||||
{"_id": org.id},
|
{"_id": org.id},
|
||||||
{"$set": {"crawlingDefaults": defaults.model_dump()}},
|
{"$set": {"crawlingDefaults": defaults.model_dump()}},
|
||||||
|
@ -16,6 +16,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
from iso639 import is_language
|
||||||
from pymongo.errors import DuplicateKeyError
|
from pymongo.errors import DuplicateKeyError
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
|
||||||
@ -193,3 +194,9 @@ def validate_regexes(regexes: List[str]):
|
|||||||
except re.error:
|
except re.error:
|
||||||
# pylint: disable=raise-missing-from
|
# pylint: disable=raise-missing-from
|
||||||
raise HTTPException(status_code=400, detail="invalid_regex")
|
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
|
remotezip
|
||||||
json-stream
|
json-stream
|
||||||
aiostream
|
aiostream
|
||||||
|
iso639-lang>=2.6.0
|
||||||
|
@ -172,6 +172,7 @@ def test_update_config_invalid_exclude_regex(
|
|||||||
assert r.status_code == 400
|
assert r.status_code == 400
|
||||||
assert r.json()["detail"] == "invalid_regex"
|
assert r.json()["detail"] == "invalid_regex"
|
||||||
|
|
||||||
|
|
||||||
def test_update_config_invalid_link_selector(
|
def test_update_config_invalid_link_selector(
|
||||||
crawler_auth_headers, default_org_id, sample_crawl_data
|
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.status_code == 400
|
||||||
assert r.json()["detail"] == "invalid_link_selector"
|
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(
|
def test_verify_default_select_links(
|
||||||
crawler_auth_headers, default_org_id, sample_crawl_data
|
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"
|
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(
|
def test_add_crawl_config_invalid_link_selectors(
|
||||||
crawler_auth_headers, default_org_id, sample_crawl_data
|
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):
|
def test_rename_org(admin_auth_headers, default_org_id):
|
||||||
UPDATED_NAME = "updated org name"
|
UPDATED_NAME = "updated org name"
|
||||||
UPDATED_SLUG = "updated-org-name"
|
UPDATED_SLUG = "updated-org-name"
|
||||||
|
@ -2340,6 +2340,9 @@ https://archiveweb.page/images/${"logo.svg"}`}
|
|||||||
"Page exclusion contains invalid regex",
|
"Page exclusion contains invalid regex",
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case APIErrorDetail.InvalidLang:
|
||||||
|
errorDetailMessage = msg("Invalid language code");
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ export type APISortQuery<T = Record<string, unknown>> = {
|
|||||||
export enum APIErrorDetail {
|
export enum APIErrorDetail {
|
||||||
InvalidLinkSelector = "invalid_link_selector",
|
InvalidLinkSelector = "invalid_link_selector",
|
||||||
InvalidRegex = "invalid_regex",
|
InvalidRegex = "invalid_regex",
|
||||||
|
InvalidLang = "invalid_lang",
|
||||||
InvalidCustomBehavior = "invalid_custom_behavior",
|
InvalidCustomBehavior = "invalid_custom_behavior",
|
||||||
CustomBehaviorNotFound = "custom_behavior_not_found",
|
CustomBehaviorNotFound = "custom_behavior_not_found",
|
||||||
CustomBehaviorBranchNotFound = "custom_behavior_branch_not_found",
|
CustomBehaviorBranchNotFound = "custom_behavior_branch_not_found",
|
||||||
|
Loading…
Reference in New Issue
Block a user