Files
django-nepkit/django_nepkit/views.py
T
sankalp 589fe8c104 feat(django_nepkit): cache parsed Nepali dates/times in admin list views and improve HTML escaping security
Cache parsed values with LRU and use per‑option format_html to prevent XSS attacks.
2026-06-06 21:51:51 +05:45

173 lines
5.6 KiB
Python

import logging
from django.http import HttpResponse, JsonResponse
from django.utils.html import format_html
from django_nepkit.conf import nepkit_settings
from django_nepkit.utils import (
get_districts_by_province,
get_municipalities_by_district,
)
logger = logging.getLogger(__name__)
# Accepted values for ``?ne=`` / ``?en=`` query params. Anything else is
# either treated as "not provided" (default) or, with ``strict=True``,
# rejected.
_TRUE_VALUES = {"true", "1", "yes", "on"}
_FALSE_VALUES = {"false", "0", "no", "off"}
def _coerce_bool(raw):
"""Validate a boolean query string value.
Returns:
``True``/``False`` if the value is recognised, ``None`` otherwise.
"""
if raw is None:
return None
lowered = raw.lower()
if lowered in _TRUE_VALUES:
return True
if lowered in _FALSE_VALUES:
return False
return None
def _render_options(data, placeholder):
"""Render a list of location options as HTML.
Each ``<option>`` is built with an explicit ``format_html`` call so
that the attribute (``value="..."``) and the text node are
individually escaped — a user-supplied data row that contains a
``"><script>`` payload can never smuggle markup into the rendered
page, regardless of which slot it ends up in. ``format_html_join``
is intentionally avoided: the per-option ``format_html`` makes the
escaping contract obvious at the call site and avoids the
placeholder/value/template-coupling that ``format_html_join``
forces when the template changes.
"""
parts: list[str] = [format_html('<option value="">{0}</option>', placeholder or "")]
for item in data or []:
opt_id = (item.get("id") if isinstance(item, dict) else "") or ""
opt_text = (item.get("text") if isinstance(item, dict) else "") or ""
parts.append(format_html('<option value="{0}">{1}</option>', opt_id, opt_text))
return HttpResponse("\n".join(parts), content_type="text/html")
def _get_primary_param(request, param_name, exclude_params=None):
"""
Extract a parameter from ``request.GET``.
The primary parameter is read first. If it is not present, the helper
falls back to the **single** remaining non-internal parameter — this
is a soft contract that makes the chained-select endpoints
friendlier to hand-written URLs (``?Bagmati``), but it is **not**
used when the query string has multiple non-internal keys (which
would make the result ambiguous).
"""
from django_nepkit.constants import INTERNAL_PARAMS
if exclude_params is None:
exclude_params = INTERNAL_PARAMS
value = request.GET.get(param_name)
if value:
return value
non_internal = [
(k, v)
for k, v in request.GET.items()
if k not in exclude_params and v and k != param_name
]
if len(non_internal) == 1:
return non_internal[0][1]
return None
def _parse_language_params(request, strict=False):
"""
Parse the ``?ne=`` / ``?en=`` query parameters.
Args:
request: Django request.
strict: When ``True``, raise ``ValueError`` if the value is
present but unrecognised (anything outside
``true|false|1|0|yes|no|on|off``). When ``False`` (the
default), unrecognised values fall back to the project
default so existing integrations aren't broken.
Returns:
Tuple of ``(ne, en)`` booleans.
"""
default_lang = nepkit_settings.DEFAULT_LANGUAGE
ne_raw = request.GET.get("ne")
en_raw = request.GET.get("en")
ne_value = _coerce_bool(ne_raw)
en_value = _coerce_bool(en_raw)
if strict and ne_raw is not None and ne_value is None:
raise ValueError(f"Invalid value for ?ne=: {ne_raw!r}")
if strict and en_raw is not None and en_value is None:
raise ValueError(f"Invalid value for ?en=: {en_raw!r}")
if ne_value is None:
ne_value = default_lang == "ne"
if en_value is None:
en_value = not ne_value
return ne_value, en_value
def _should_return_html(request):
"""Decide whether to return HTML (for HTMX / direct include) or JSON."""
if request.headers.get("HX-Request") == "true":
return True
return request.GET.get("html", "false").lower() == "true"
def _location_list_view(request, param_name, data_func, placeholders):
"""Generic handler for the province→district→municipality chain."""
param_value = _get_primary_param(request, param_name)
if not param_value:
return JsonResponse([], safe=False)
ne, en = _parse_language_params(request)
as_html = _should_return_html(request)
data = data_func(param_value, ne=ne, en=en)
if as_html:
placeholder = placeholders[0] if ne else placeholders[1]
return _render_options(data, placeholder)
return JsonResponse(data, safe=False)
def district_list_view(request):
"""Return districts for a given province."""
from django_nepkit.constants import PLACEHOLDERS
return _location_list_view(
request,
param_name="province",
data_func=get_districts_by_province,
placeholders=(PLACEHOLDERS["district"]["ne"], PLACEHOLDERS["district"]["en"]),
)
def municipality_list_view(request):
"""Return municipalities for a given district."""
from django_nepkit.constants import PLACEHOLDERS
return _location_list_view(
request,
param_name="district",
data_func=get_municipalities_by_district,
placeholders=(
PLACEHOLDERS["municipality"]["ne"],
PLACEHOLDERS["municipality"]["en"],
),
)