Skip to content

Commit 0c7619f

Browse files
committed
fix: handle escaped urls
1 parent 1acc0c5 commit 0c7619f

11 files changed

Lines changed: 130 additions & 24 deletions

File tree

LOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
Questo file documenta gli avanzamenti significativi e le decisioni chiave del progetto `normattiva_2_md`.
44

5+
## 2026-01-12
6+
7+
### URL con escape backslash
8+
9+
- Normalizzazione URL normattiva.it con backslash in CLI/API
10+
- Aggiornati test sicurezza per URL con escape
11+
512
## 2026-01-11
613

714
### Coverage Reporting Implementation
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Update escaped URL handling
2+
3+
## Summary
4+
Allow conversion when normattiva URLs include backslash-escaped delimiters by normalizing them before validation and download.
5+
6+
## Motivation
7+
8+
- Some terminals or copy sources escape `?`, `;`, `!`, `=` with backslashes, causing URL validation or download failures.
9+
- Normalize input to reduce user friction and make pasted links work reliably.
10+
11+
## Impact
12+
13+
- CLI/API input handling: normalize escaped URLs before `validate_normattiva_url` and `extract_params_from_normattiva_url`.
14+
- Tests: add coverage for escaped URL input.
15+
16+
## Open Questions / Risks
17+
18+
- Should normalization remove only backslashes before reserved delimiters or strip all backslashes in URLs?
19+
- Should the normalized URL be echoed in logs/metadata instead of the raw input?
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# markdown-conversion Specification Delta
2+
3+
## ADDED Requirements
4+
5+
### Requirement: Escaped Normattiva URL Normalization
6+
The system SHALL normalize normattiva.it URLs containing escape characters from user input before validation and download.
7+
8+
#### Scenario: Backslash-escaped URL from clipboard
9+
- **WHEN** user provides a normattiva.it URL containing backslash-escaped reserved characters (for example `\?`, `\;`, `\!`, `\=`)
10+
- **THEN** the system SHALL remove the escape backslashes to produce a valid URL
11+
- **AND** proceed with validation and download using the normalized URL
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Tasks for update-url-escape-handling
2+
3+
## URL Normalization
4+
5+
- [ ] Identify URL intake points (CLI, `convert_url`, `is_normattiva_url`) and add a normalization helper for escaped delimiters.
6+
- [ ] Normalize backslash-escaped reserved characters before validation and download.
7+
8+
## Error Messaging
9+
10+
- [ ] Ensure logs and metadata use the normalized URL consistently.
11+
12+
## Validation
13+
14+
- [ ] Add a unit or CLI test for escaped URL input (use the example from the request).
15+
- [ ] Run relevant tests (`python -m unittest ...` or `make test`).

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "normattiva2md"
7-
version = "2.1.5"
7+
version = "2.1.6"
88
description = "Convertitore da XML Akoma Ntoso a formato Markdown con download automatico delle leggi citate e cross-references inline (CLI: normattiva2md)"
99
readme = "README.md"
1010
requires-python = ">=3.7"

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def read_readme():
1919

2020
setup(
2121
name="normattiva2md",
22-
version="2.1.5",
22+
version="2.1.6",
2323
description="Convertitore da XML Akoma Ntoso a formato Markdown con download automatico delle leggi citate e cross-references inline (CLI: normattiva2md)",
2424
long_description=read_readme(),
2525
long_description_content_type="text/markdown",

src/normattiva2md/api.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,15 @@
3737
download_akoma_ntoso,
3838
extract_params_from_normattiva_url,
3939
is_normattiva_url,
40+
normalize_normattiva_url,
4041
validate_normattiva_url,
4142
)
4243
from .utils import load_env_file
43-
from .xml_parser import construct_article_eid, extract_metadata_from_xml, filter_xml_to_article
44+
from .xml_parser import (
45+
construct_article_eid,
46+
extract_metadata_from_xml,
47+
filter_xml_to_article,
48+
)
4449

4550
logger = logging.getLogger(__name__)
4651

@@ -81,22 +86,24 @@ def convert_url(
8186
# Load .env file for API keys
8287
load_env_file()
8388

89+
normalized_url = normalize_normattiva_url(url)
90+
8491
# Validate URL
8592
try:
86-
validate_normattiva_url(url)
93+
validate_normattiva_url(normalized_url)
8794
except ValueError as e:
8895
raise InvalidURLError(
89-
f"URL non valido: {e}. "
90-
f"L'URL deve essere HTTPS e dominio normattiva.it"
96+
f"URL non valido: {e}. L'URL deve essere HTTPS e dominio normattiva.it"
9197
)
9298

9399
if not quiet:
94-
logger.info(f"Conversione URL: {url}")
100+
logger.info(f"Conversione URL: {normalized_url}")
95101

96102
# Extract parameters from URL
97-
params, session = extract_params_from_normattiva_url(url, quiet=quiet)
103+
params, session = extract_params_from_normattiva_url(normalized_url, quiet=quiet)
104+
98105
if not params:
99-
logger.warning(f"Impossibile estrarre parametri da {url}")
106+
logger.warning(f"Impossibile estrarre parametri da {normalized_url}")
100107
return None
101108

102109
# Download XML to temp file
@@ -114,7 +121,7 @@ def convert_url(
114121
"dataGU": params["dataGU"],
115122
"codiceRedaz": params["codiceRedaz"],
116123
"dataVigenza": params["dataVigenza"],
117-
"url": url,
124+
"url": normalized_url,
118125
"url_xml": f"https://www.normattiva.it/do/atto/caricaAKN?dataGU={params['dataGU']}&codiceRedaz={params['codiceRedaz']}&dataVigenza={params['dataVigenza']}",
119126
}
120127

@@ -132,7 +139,7 @@ def convert_url(
132139
)
133140

134141
if result:
135-
result.url = url
142+
result.url = normalized_url
136143
result.url_xml = metadata["url_xml"]
137144

138145
return result
@@ -182,8 +189,7 @@ def convert_xml(
182189
# Check file exists
183190
if not os.path.exists(xml_path):
184191
raise XMLFileNotFoundError(
185-
f"File XML non trovato: '{xml_path}'. "
186-
f"Verifica che il path sia corretto."
192+
f"File XML non trovato: '{xml_path}'. Verifica che il path sia corretto."
187193
)
188194

189195
if not quiet:
@@ -352,7 +358,9 @@ def search_law(
352358
)
353359

354360
if response.status_code != 200:
355-
logger.warning(f"Errore Exa API (HTTP {response.status_code}): {response.text}")
361+
logger.warning(
362+
f"Errore Exa API (HTTP {response.status_code}): {response.text}"
363+
)
356364
return []
357365

358366
data = response.json()
@@ -373,7 +381,9 @@ def search_law(
373381
query_lower,
374382
re.IGNORECASE,
375383
)
376-
requested_article = article_match.group(1).replace(" ", "") if article_match else None
384+
requested_article = (
385+
article_match.group(1).replace(" ", "") if article_match else None
386+
)
377387

378388
for result in raw_results:
379389
result_url = result.get("url")

src/normattiva2md/cli.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ def print_rich_help():
4242
# Legal warning
4343
warning_text = Text()
4444
warning_text.append(
45-
"⚠️ I testi presenti nella banca dati \"Normattiva\" non hanno carattere di ufficialità.\n"
45+
'⚠️ I testi presenti nella banca dati "Normattiva" non hanno carattere di ufficialità.\n'
4646
"L'unico testo ufficiale e definitivo è quello pubblicato sulla Gazzetta Ufficiale Italiana.",
47-
style="bold yellow on dark_red"
47+
style="bold yellow on dark_red",
4848
)
4949
warning_panel = Panel(
5050
warning_text,
@@ -183,7 +183,7 @@ def print_rich_help():
183183
footer_text.append("↑↓ PgUp/PgDn Spazio ", style="dim yellow")
184184
footer_text.append("| Esci: ", style="dim italic")
185185
footer_text.append("q", style="dim yellow")
186-
186+
187187
footer = Panel(
188188
footer_text,
189189
border_style="dim",
@@ -203,6 +203,7 @@ def print_rich_help():
203203
from .utils import sanitize_output_path, generate_snake_case_filename, load_env_file
204204
from .normattiva_api import (
205205
is_normattiva_url,
206+
normalize_normattiva_url,
206207
validate_normattiva_url,
207208
extract_params_from_normattiva_url,
208209
download_akoma_ntoso,
@@ -599,6 +600,7 @@ def main():
599600

600601
# Auto-detect: URL o file locale?
601602
if is_normattiva_url(input_source):
603+
input_source = normalize_normattiva_url(input_source)
602604
# Gestione URL
603605
if not quiet_mode:
604606
print(f"Rilevato URL normattiva.it: {input_source}", file=sys.stderr)

src/normattiva2md/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
MAX_FILE_SIZE_MB = 50
88
MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024
99
DEFAULT_TIMEOUT = 30
10-
VERSION = "2.1.5"
10+
VERSION = "2.1.6"

src/normattiva2md/normattiva_api.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,35 @@
1+
import os
12
import re
23
import sys
4+
from datetime import datetime
35
from urllib.parse import urlparse
4-
from .constants import ALLOWED_DOMAINS, DEFAULT_TIMEOUT, VERSION, MAX_FILE_SIZE_BYTES, MAX_FILE_SIZE_MB
6+
57
import requests
6-
import os
7-
from datetime import datetime
8+
9+
from .constants import (
10+
ALLOWED_DOMAINS,
11+
DEFAULT_TIMEOUT,
12+
VERSION,
13+
MAX_FILE_SIZE_BYTES,
14+
MAX_FILE_SIZE_MB,
15+
)
16+
17+
18+
def normalize_normattiva_url(url):
19+
"""
20+
Normalizza URL normattiva.it rimuovendo escape backslash.
21+
22+
Args:
23+
url: URL string
24+
25+
Returns:
26+
str: URL normalizzato
27+
"""
28+
if not isinstance(url, str):
29+
return url
30+
31+
return url.replace("\\", "")
32+
833

934
def validate_normattiva_url(url):
1035
"""
@@ -20,6 +45,7 @@ def validate_normattiva_url(url):
2045
ValueError: If URL is invalid or not from allowed domain
2146
"""
2247
try:
48+
url = normalize_normattiva_url(url)
2349
parsed = urlparse(url)
2450

2551
# Check scheme is HTTPS
@@ -39,6 +65,7 @@ def validate_normattiva_url(url):
3965
except Exception as e:
4066
raise ValueError(f"URL non valido: {e}")
4167

68+
4269
def is_normattiva_url(input_str):
4370
"""
4471
Verifica se l'input è un URL di normattiva.it
@@ -52,17 +79,20 @@ def is_normattiva_url(input_str):
5279
if not isinstance(input_str, str):
5380
return False
5481

82+
normalized = normalize_normattiva_url(input_str)
83+
5584
# Check if it looks like a URL
56-
if not re.match(r"https?://(www\.)?normattiva\.it/", input_str, re.IGNORECASE):
85+
if not re.match(r"https?://(www\.)?normattiva\.it/", normalized, re.IGNORECASE):
5786
return False
5887

5988
# Validate URL for security
6089
try:
61-
validate_normattiva_url(input_str)
90+
validate_normattiva_url(normalized)
6291
return True
6392
except ValueError:
6493
return False
6594

95+
6696
def is_normattiva_export_url(url):
6797
"""
6898
Verifica se l'URL è un URL di esportazione atto intero di normattiva.it
@@ -82,6 +112,7 @@ def is_normattiva_export_url(url):
82112
# Check if it's an export URL
83113
return "/esporta/attoCompleto" in url and is_normattiva_url(url)
84114

115+
85116
def extract_params_from_normattiva_url(url, session=None, quiet=False):
86117
"""
87118
Scarica la pagina normattiva e estrae i parametri necessari per il download
@@ -100,6 +131,8 @@ def extract_params_from_normattiva_url(url, session=None, quiet=False):
100131
Returns:
101132
tuple: (params dict, session)
102133
"""
134+
url = normalize_normattiva_url(url)
135+
103136
# Reject export URLs as they require authentication
104137
if is_normattiva_export_url(url):
105138
print(

0 commit comments

Comments
 (0)