Source code for org.forms.page
from __future__ import annotations
from fnmatch import fnmatch
from urllib.parse import urlparse
from onegov.form import Form
from onegov.form.fields import ChosenSelectField
from onegov.form.fields import TagsField
from onegov.form.fields import URLField
from onegov.org import _
from onegov.org.forms.fields import HtmlField
from onegov.form.fields import PanelField
from onegov.org.forms.generic import ChangeAdjacencyListUrlForm
from onegov.page.collection import PageCollection
from wtforms.fields import BooleanField
from wtforms.fields import StringField
from wtforms.fields import TextAreaField
from wtforms.validators import InputRequired
from wtforms.validators import URL
from wtforms.validators import ValidationError
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Iterable, Iterator
from onegov.page import Page
[docs]
class PageBaseForm(Form):
""" Defines the base form for all pages. """
[docs]
title = StringField(
label=_('Title'),
validators=[InputRequired()],
render_kw={'autofocus': ''}
)
[docs]
class LinkForm(PageBaseForm):
""" Defines the form for pages with the 'link' trait. """
[docs]
url = URLField(
label=_('URL'),
default_scheme=None,
validators=[InputRequired(), URL(require_tld=False)],
render_kw={'class_': 'image-url file-url internal-url'}
)
[docs]
page_image = StringField(
label=_('Image'),
render_kw={'class_': 'image-url'},
description=_(
'Will be used as image in the page overview on the parent page')
)
[docs]
class PageForm(PageBaseForm):
""" Defines the form for pages with the 'page' trait. """
[docs]
lead = TextAreaField(
label=_('Lead'),
description=_('Describes what this page is about'),
render_kw={'rows': 4})
[docs]
keywords = TagsField(
label=_('Keywords'),
description=_(
'Additional search terms and synonyms for this page. '
'Helps visitors find this page even if they use different words.'
)
)
[docs]
class IframeForm(PageBaseForm):
""" Defines the form for pages with the 'iframe' trait. """
[docs]
lead = TextAreaField(
label=_('Lead'),
description=_('Describes what this page is about'),
render_kw={'rows': 4})
[docs]
domain_hint = PanelField(
text=_('There are currently no allowed domains for iFrames. To enable '
'domains for iFrames, please contact info@seantis.ch.'),
kind='callout',
fieldset=_('URL')
)
[docs]
url = URLField(
label=_('URL'),
default_scheme=None,
validators=[InputRequired(), URL(require_tld=False)],
fieldset=_('URL')
)
[docs]
height = StringField(
label=_('Height'),
description=_('The height of the iFrame in pixels. '
'If not set, the iFrame will have a standard height of '
'800px.'),
render_kw={'placeholder': 'auto'},
fieldset=_('Display')
)
[docs]
as_card = BooleanField(
label=_('Display as card'),
description=_('Display the iFrame as a card with a border'),
fieldset=_('Display')
)
[docs]
def on_request(self) -> None:
self.allowed_domains = (
self.request.app.allowed_iframe_domains) # type: ignore
for domain in getattr(
self.request.app.settings.content_security_policy.default,
'child_src', set()):
self.allowed_domains.append(domain) if domain != "'self'" else None
if self.allowed_domains:
# keep displayed domain order stable
self.allowed_domains.sort()
self.domain_hint.text = (
self.request.translate(
_('The following domains are allowed for iFrames:')
) + '\n - '
+ '\n - '.join(self.allowed_domains)
+ '\n\n'
+ self.request.translate(
_('To allow more domains for iFrames, please contact '
'info@seantis.ch.'))
)
[docs]
def validate_url(self, field: URLField) -> None:
""" Validates the URL against the iFrame allow-list.
This implements only a subset of CSP source matching: the scheme
(an ``http`` source also allows ``https``, an empty scheme allows
any) and the host (with wildcard support, e.g. ``*.vimeo.com``).
Ports and paths on the allow-list entries are not honoured.
"""
if not field.data:
return
url = urlparse(field.data)
if url.hostname is None:
raise ValidationError(
_('The domain of the URL is not allowed for iFrames.')
)
for allowed in self.allowed_domains:
allowed_url = urlparse(allowed)
if allowed_url.hostname is None:
continue
scheme_matches = (
not allowed_url.scheme
or url.scheme == allowed_url.scheme
# an http source also allows the https upgrade
or (allowed_url.scheme == 'http' and url.scheme == 'https')
)
# CSP child_src entries may use a wildcard host (*.vimeo.com)
if scheme_matches and fnmatch(url.hostname, allowed_url.hostname):
return
raise ValidationError(
_('The domain of the URL is not allowed for iFrames.')
)
[docs]
class MovePageForm(Form):
""" Form to move a page including its subpages. """
[docs]
parent_id = ChosenSelectField(
label=_('Destination'),
coerce=int,
choices=[],
validators=[
InputRequired()
]
)
[docs]
def on_request(self) -> None:
pages = PageCollection(self.request.session)
self.parent_id.choices = list(self.iterate_page_tree(pages.roots))
# adding root element, ids start beyond 0, so 0 means no parent
self.parent_id.choices.insert(
0, (0, self.request.translate(_('- Root -')))
)
[docs]
def iterate_page_tree(
self,
pages: Iterable[Page],
indent: str = '',
) -> Iterator[tuple[int, str]]:
"""
Iterates over the page tree and lists the elements with ident
to show the page hierarchy in the choices list
"""
from onegov.org.models import News
for page in pages:
if isinstance(page, News):
continue # prevent pages to be moved under a news page
yield page.id, f'{indent} {page.title}'
yield from self.iterate_page_tree(
page.children,
indent=indent + ' -'
)
[docs]
def validate_parent_id(self, field: ChosenSelectField) -> None:
"""
As a new destination (parent page) every menu item is valid except
yourself or a child of yourself.
"""
if self.parent_id.data:
new_parent_id = int(self.parent_id.data)
# prevent selecting yourself as new parent
if self.model.page_id == new_parent_id:
raise ValidationError(_('Invalid destination selected'))
# prevent selecting a child node
if any(
choice[0] == new_parent_id
for choice in self.iterate_page_tree(self.model.page.children)
):
raise ValidationError(_('Invalid destination selected'))
[docs]
def update_model(self, model: Page) -> None:
session = self.request.session
pages = PageCollection(session)
new_parent_id = None
new_parent = None
if self.parent_id.data:
new_parent_id = self.parent_id.data
new_parent = pages.by_id(new_parent_id)
model.name = pages.get_unique_child_name(model.title, new_parent)
model.parent_id = new_parent_id