forked from ungleich-public/dynamicweb
117 lines
4.4 KiB
Python
117 lines
4.4 KiB
Python
|
|
from typing import TYPE_CHECKING, Optional
|
||
|
|
from urllib.parse import urlsplit
|
||
|
|
|
||
|
|
from django.core.exceptions import ValidationError
|
||
|
|
from django.core.validators import RegexValidator, URLValidator
|
||
|
|
from django.utils.html import escape
|
||
|
|
from django.utils.safestring import mark_safe
|
||
|
|
from django.utils.translation import gettext
|
||
|
|
|
||
|
|
from cms.utils.urlutils import admin_reverse, path_segment_regex, relative_url_regex
|
||
|
|
|
||
|
|
if TYPE_CHECKING:
|
||
|
|
# Only needed for type hinting - avoid circular import
|
||
|
|
from cms.models.pagemodel import Page
|
||
|
|
|
||
|
|
|
||
|
|
def validate_relative_url(value):
|
||
|
|
RegexValidator(regex=relative_url_regex)(value)
|
||
|
|
|
||
|
|
|
||
|
|
def validate_url(value):
|
||
|
|
if "<" in value or ">" in value:
|
||
|
|
# ``relative_url_regex`` excludes angle brackets, but the absolute fallback below
|
||
|
|
# is Django's ``URLValidator``, whose path pattern accepts them as long as the
|
||
|
|
# value carries no space (``https://example.com/<script>alert(1)</script>``).
|
||
|
|
# Reject them before either branch runs, so markup cannot enter through a scheme.
|
||
|
|
raise ValidationError(
|
||
|
|
gettext("Enter a valid relative or absolute URL."),
|
||
|
|
code="invalid",
|
||
|
|
)
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Validate relative urls first
|
||
|
|
validate_relative_url(value)
|
||
|
|
except ValidationError:
|
||
|
|
# Fallback to absolute urls
|
||
|
|
URLValidator()(value)
|
||
|
|
else:
|
||
|
|
# ``relative_url_regex`` only constrains the *characters* of a relative
|
||
|
|
# path, not the part before the first slash. Values such as
|
||
|
|
# ``javascript:alert(1)/x``, ``data:text/html,...`` or ``vbscript:.../x``
|
||
|
|
# therefore match the relative branch while still carrying a scheme that
|
||
|
|
# executes script when the value is later rendered into an ``href``
|
||
|
|
# (CWE-79). A genuine relative URL has no scheme, so reject any value
|
||
|
|
# that does. (Off-site targets such as ``https://example.com`` or
|
||
|
|
# ``//example.com`` are intentionally permitted -- this field allows
|
||
|
|
# absolute redirects -- and navigate rather than execute script.)
|
||
|
|
if urlsplit(value).scheme:
|
||
|
|
raise ValidationError(
|
||
|
|
gettext("Enter a valid relative or absolute URL."),
|
||
|
|
code="invalid",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def validate_path_segment(value):
|
||
|
|
"""Validate a single path segment, i.e. a path that contains no "/"."""
|
||
|
|
RegexValidator(
|
||
|
|
regex=path_segment_regex,
|
||
|
|
message=gettext("Enter a valid relative or absolute URL."),
|
||
|
|
code="invalid",
|
||
|
|
)(value)
|
||
|
|
|
||
|
|
|
||
|
|
def validate_path(value):
|
||
|
|
"""Validate a page path.
|
||
|
|
|
||
|
|
Unlike :func:`validate_url` this also accepts a single path segment such as ``contact``,
|
||
|
|
which ``relative_url_regex`` -- and therefore :func:`validate_url` -- never matches.
|
||
|
|
"""
|
||
|
|
if "/" in value:
|
||
|
|
validate_url(value)
|
||
|
|
else:
|
||
|
|
validate_path_segment(value)
|
||
|
|
|
||
|
|
|
||
|
|
def validate_url_uniqueness(
|
||
|
|
site, path: str, language: str, user_language: str | None = None, exclude_page: Optional["Page"] = None
|
||
|
|
):
|
||
|
|
"""Checks for conflicting urls"""
|
||
|
|
from cms.models.pagemodel import PageUrl
|
||
|
|
|
||
|
|
# Single-segment paths used to skip validation entirely, so an "Overwrite URL" without a
|
||
|
|
# "/" could hold arbitrary characters.
|
||
|
|
validate_path(path)
|
||
|
|
|
||
|
|
path = path.strip("/")
|
||
|
|
page_urls = PageUrl.objects.get_for_site(site, language=language).filter(path=path)
|
||
|
|
|
||
|
|
if exclude_page:
|
||
|
|
page_urls = page_urls.exclude(page=exclude_page.pk)
|
||
|
|
|
||
|
|
try:
|
||
|
|
conflict_page = page_urls[0].page
|
||
|
|
except IndexError:
|
||
|
|
return True
|
||
|
|
|
||
|
|
conflict_translation = conflict_page.get_content_obj(language, fallback=False)
|
||
|
|
|
||
|
|
if conflict_translation: # No empty page content
|
||
|
|
change_url = admin_reverse("cms_pagecontent_change", args=[conflict_translation.pk])
|
||
|
|
else:
|
||
|
|
change_url = "" # Empty page has no slug
|
||
|
|
if user_language:
|
||
|
|
change_url += f"?language={user_language}"
|
||
|
|
|
||
|
|
conflict_url = f'<a href="{change_url}" target="_blank">{escape(str(conflict_translation.title))}</a>'
|
||
|
|
|
||
|
|
if exclude_page:
|
||
|
|
message = gettext("Page %(conflict_page)s has the same url '%(url)s' as current page \"%(instance)s\".")
|
||
|
|
else:
|
||
|
|
message = gettext("Page %(conflict_page)s has the same url '%(url)s' as current page.")
|
||
|
|
message = message % {
|
||
|
|
"conflict_page": conflict_url,
|
||
|
|
"url": escape(path),
|
||
|
|
"instance": escape(exclude_page.get_title(language)) if exclude_page else "",
|
||
|
|
}
|
||
|
|
raise ValidationError(mark_safe(message))
|