Verified Commit 41cbe6d2 authored by Štěpán Henek's avatar Štěpán Henek 🌩

config pages splitted to separate modules

parent d84d8f69
This diff is collapsed.
#
# Foris
# Copyright (C) 2019 CZ.NIC, z.s.p.o. <http://www.nic.cz>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .base import ConfigPageMixin, JoinedPages
from foris.state import current_state
from foris.utils.translators import gettext_dummy as gettext
class AboutConfigPage(ConfigPageMixin):
slug = "about"
menu_order = 99
template = "config/about"
template_type = "jinja2"
userfriendly_title = gettext("About")
def render(self, **kwargs):
data = current_state.backend.perform("about", "get")
# process dates etc
return self.default_template(data=data, **kwargs)
#
# Foris
# Copyright (C) 2019 CZ.NIC, z.s.p.o. <http://www.nic.cz>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import bottle
import typing
from bottle import template
from foris import fapi
from foris.state import current_state
from foris.utils import messages
from foris.utils.translators import _
class BaseConfigPage(object):
menu_order = 50
slug: typing.Optional[str] = None
page_name: typing.Optional[str] = None
userfriendly_title: typing.Optional[str]
menu_title: typing.Optional[str] = None
subpages: typing.Iterable[typing.Type['ConfigPageMixin']] = []
@staticmethod
def get_menu_tag_static(cls):
if current_state.guide.enabled and current_state.guide.current == cls.slug:
return {
"show": True,
"hint": "",
"text": "<i class='fas fa-reply'></i>",
}
else:
return {
"show": False,
"hint": "",
"text": "",
}
@classmethod
def get_menu_tag(cls):
return ConfigPageMixin.get_menu_tag_static(cls)
@staticmethod
def is_visible_static(cls):
if current_state.guide.enabled:
return cls.slug in current_state.guide.steps
return True
@classmethod
def is_visible(cls):
return ConfigPageMixin.is_visible_static(cls)
@staticmethod
def is_enabled_static(cls):
if current_state.guide.enabled:
return cls.slug in current_state.guide.available_tabs
return True
@classmethod
def is_enabled(cls):
return ConfigPageMixin.is_enabled_static(cls)
@classmethod
def subpage_slugs(cls):
return [e.slug for e in cls.subpages]
class ConfigPageMixin(BaseConfigPage):
# page url part /config/<slug>
template = "config/main"
template_type = "simple"
def call_action(self, action):
"""Call config page action.
:param action:
:return: object that can be passed as HTTP response to Bottle
"""
raise bottle.HTTPError(404, "No actions specified for this page.")
def call_ajax_action(self, action):
"""Call AJAX action.
:param action:
:return: dict of picklable AJAX results
"""
raise bottle.HTTPError(404, "No AJAX actions specified for this page.")
def get_page_form(self, form_name: str, data: dict, controller_id: str) -> typing.Tuple[
fapi.ForisAjaxForm, typing.Callable[[dict], typing.Tuple['str', 'str']]
]:
"""Returns appropriate foris form and handler to generate response
"""
raise bottle.HTTPError(404, "No forms specified for this page.")
def call_insecure(self, identifier):
"""Handels insecure request (no login required)
:param namespace: namespace of the storage (e.g. tokens)
:return: object that can be passed as HTTP response to Bottle
"""
raise bottle.HTTPError(404, "No storage specified for this page.")
def default_template(self, **kwargs):
if self.template_type == "jinja2":
page_template = "%s.html.j2" % self.template
kwargs['template_adapter'] = bottle.Jinja2Template
else:
page_template = self.template
return template(
page_template, title=_(kwargs.pop('title', self.userfriendly_title)), **kwargs)
def render(self, **kwargs):
try:
form = getattr(self, "form")
first_section = form.sections[0]
title = first_section.title
description = first_section.description
except (NotImplementedError, AttributeError):
form = None
title = self.userfriendly_title
description = None
return self.default_template(form=form, title=title, description=description, **kwargs)
def save(self, *args, **kwargs):
no_messages = kwargs.pop("no_messages", False)
result = super(ConfigPageMixin, self).save(*args, **kwargs)
if no_messages:
return result
if result:
messages.success(_("Configuration was successfully saved."))
else:
messages.warning(_("There were some errors in your input."))
return result
class JoinedPages(BaseConfigPage):
userfriendly_title = None
#
# Foris
# Copyright (C) 2019 CZ.NIC, z.s.p.o. <http://www.nic.cz>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from foris.config_handlers import dns
from foris.state import current_state
from .base import ConfigPageMixin
class DNSConfigPage(ConfigPageMixin, dns.DNSHandler):
slug = "dns"
menu_order = 19
template = "config/dns"
template_type = "jinja2"
def _action_check_connection(self):
return current_state.backend.perform(
"wan", "connection_test_trigger", {"test_kinds": ["dns"]})
def call_ajax_action(self, action):
if action == "check-connection":
return self._action_check_connection()
raise ValueError("Unknown AJAX action.")
#
# Foris
# Copyright (C) 2019 CZ.NIC, z.s.p.o. <http://www.nic.cz>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
from foris.config_handlers import guest
from .base import ConfigPageMixin
class GuestConfigPage(ConfigPageMixin, guest.GuestHandler):
slug = "guest"
menu_order = 17
template = "config/guest"
template_type = "jinja2"
def render(self, **kwargs):
kwargs["dhcp_clients"] = self.backend_data["dhcp"]["clients"]
kwargs["interface_count"] = self.backend_data["interface_count"]
kwargs["interface_up_count"] = self.backend_data["interface_up_count"]
for client in kwargs["dhcp_clients"]:
client["expires"] = datetime.utcfromtimestamp(client["expires"]).strftime(
"%Y-%m-%d %H:%M"
)
return super(GuestConfigPage, self).render(**kwargs)
#
# Foris
# Copyright (C) 2019 CZ.NIC, z.s.p.o. <http://www.nic.cz>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from foris.guide import Workflow
from foris.config_handlers import profile, misc
from foris.state import current_state
from foris.utils import messages
from foris.utils.translators import _
from .base import ConfigPageMixin
class ProfileConfigPage(ConfigPageMixin, profile.ProfileHandler):
slug = "profile"
menu_order = 13
template = "config/profile"
template_type = "jinja2"
def render(self, **kwargs):
kwargs['workflows'] = [
Workflow(
e, self.backend_data["current_workflow"] == e,
self.backend_data["recommended_workflow"] == e
) for e in self.backend_data["available_workflows"]
]
# perform some workflow sorting
SCORE = {
"router": 1, # router first
"bridge": 2,
}
kwargs['workflows'].sort(key=lambda e: (SCORE.get(e.name, 99), e.name))
return super(ProfileConfigPage, self).render(**kwargs)
def save(self, *args, **kwargs):
result = super(ProfileConfigPage, self).save(no_messages=True, *args, **kwargs)
if self.form.callback_results["result"]:
messages.success(_("Guide workflow was sucessfully set."))
else:
messages.error(_("Failed to set guide workflow."))
return result
@classmethod
def is_visible(cls):
if not current_state.guide.enabled:
return False
return ConfigPageMixin.is_visible_static(cls)
@classmethod
def is_enabled(cls):
if not current_state.guide.enabled:
return False
return ConfigPageMixin.is_enabled_static(cls)
class GuideFinishedPage(ConfigPageMixin, misc.GuideFinishedHandler):
slug = "finished"
menu_order = 90
template_type = "jinja2"
template = "config/finished"
def save(self, *args, **kwargs):
result = super().save(no_messages=True, *args, **kwargs)
if not self.form.callback_results["result"]:
messages.error(_("Failed to finish the guide."))
return result
@classmethod
def is_visible(cls):
if not current_state.guide.enabled:
return False
return ConfigPageMixin.is_visible_static(cls)
@classmethod
def is_enabled(cls):
if not current_state.guide.enabled:
return False
return ConfigPageMixin.is_enabled_static(cls)
#
# Foris
# Copyright (C) 2019 CZ.NIC, z.s.p.o. <http://www.nic.cz>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
from foris.config_handlers import lan
from .base import ConfigPageMixin
class LanConfigPage(ConfigPageMixin, lan.LanHandler):
slug = "lan"
menu_order = 16
template = "config/lan"
template_type = "jinja2"
def render(self, **kwargs):
kwargs["dhcp_clients"] = self.backend_data["mode_managed"]["dhcp"]["clients"]
kwargs["interface_count"] = self.backend_data["interface_count"]
kwargs["interface_up_count"] = self.backend_data["interface_up_count"]
for client in kwargs["dhcp_clients"]:
client["expires"] = datetime.utcfromtimestamp(client["expires"]).strftime(
"%Y-%m-%d %H:%M"
)
return super(LanConfigPage, self).render(**kwargs)
#
# Foris
# Copyright (C) 2019 CZ.NIC, z.s.p.o. <http://www.nic.cz>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import bottle
import base64
from datetime import datetime
from foris.config_handlers import backups, notifications
from foris.state import current_state
from foris.utils import messages
from foris.utils.routing import reverse
from foris.utils.translators import gettext_dummy as gettext, _
from .base import ConfigPageMixin
class MaintenanceConfigPage(ConfigPageMixin, backups.MaintenanceHandler):
slug = "maintenance"
menu_order = 21
template = "config/maintenance"
template_type = "jinja2"
userfriendly_title = gettext("Maintenance")
def _action_config_backup(self):
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = "turris-backup-%s.tar.bz2" % timestamp
data = current_state.backend.perform("maintain", "generate_backup")
raw_data = base64.b64decode(data["backup"])
bottle.response.set_header("Content-Type", "application/x-bz2")
bottle.response.set_header("Content-Disposition", 'attachment; filename="%s"' % filename)
bottle.response.set_header("Content-Length", len(raw_data))
return raw_data
def _action_save_notifications(self):
if bottle.request.method != 'POST':
messages.error(_("Wrong HTTP method."))
bottle.redirect(reverse("config_page", page_name="maintenance"))
handler = notifications.NotificationsHandler(bottle.request.POST.decode())
if handler.save():
messages.success(_("Configuration was successfully saved."))
bottle.redirect(reverse("config_page", page_name="maintenance"))
messages.warning(_("There were some errors in your input."))
return super(MaintenanceConfigPage, self).render(notifications_form=handler.form)
def _action_test_notifications(self):
if bottle.request.method != 'POST':
messages.error(_("Wrong HTTP method."))
bottle.redirect(reverse("config_page", page_name="maintenance"))
data = current_state.backend.perform(
"router_notifications", "create",
{
"msg": "_(This is a testing notification. Please ignore me.)",
"severity": "news",
"immediate": True,
}
)
if data["result"]:
messages.success(_("Testing message was sent, please check your inbox."))
else:
messages.error(_(
"Sending of the testing message failed, your configuration is possibly wrong."
))
bottle.redirect(reverse("config_page", page_name="maintenance"))
def call_action(self, action):
if action == "config-backup":
return self._action_config_backup()
elif action == "save_notifications":
return self._action_save_notifications()
elif action == "test_notifications":
return self._action_test_notifications()
raise ValueError("Unknown AJAX action.")
def render(self, **kwargs):
notifications_handler = notifications.NotificationsHandler(self.data)
return super(MaintenanceConfigPage, self).render(
notifications_form=notifications_handler.form,
**kwargs
)
def save(self, *args, **kwargs):
super(MaintenanceConfigPage, self).save(no_messages=True, *args, **kwargs)
result = self.form.callback_results.get('result')
if result:
messages.success(_("Configuration was successfully restored. "
"Note that a reboot will be required to apply restored "
"configuration."))
else:
messages.warning(_("Failed to restore the backup from the provided file."))
return result
#
# Foris
# Copyright (C) 2019 CZ.NIC, z.s.p.o. <http://www.nic.cz>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from foris.config_handlers import networks
from foris.state import current_state
from foris.utils import messages
from foris.utils.translators import _
from .base import ConfigPageMixin
class NetworksConfigPage(ConfigPageMixin, networks.NetworksHandler):
slug = "networks"
menu_order = 14
template = "config/networks"
template_type = "jinja2"
def render(self, **kwargs):
# place non-configurable intefaces in front of configurable
kwargs['networks'] = {}
for network_name in self.backend_data["networks"].keys():
kwargs['networks'][network_name] = sorted(
self.backend_data["networks"][network_name],
key=lambda x: (1 if x["configurable"] else 0, x["slot"]), reverse=False
)
for network in kwargs['networks'][network_name]:
if network["type"] == "wifi":
network["slot"] = network["bus"] + network["slot"]
# don't display inconfigurable devices in none network (can't be configured anyway)
kwargs['networks']['none'] = [
e for e in kwargs['networks']['none'] if e["configurable"]
]
return super(NetworksConfigPage, self).render(**kwargs)
def save(self, *args, **kwargs):
result = super(NetworksConfigPage, self).save(no_messages=True, *args, **kwargs)
if self.form.callback_results["result"]:
messages.success(_("Network configuration was sucessfully updated."))
else:
messages.error(_("Unable to update your network configuration."))
return result
@classmethod
def is_enabled(cls):
if current_state.device in ["turris"]:
return False
# Don't show in turrisOS version < "4.0"
if int(current_state.turris_os_version.split(".", 1)[0]) < 4:
return False
return ConfigPageMixin.is_enabled_static(cls)
@classmethod
def is_visible(cls):
return cls.is_enabled()
#
# Foris
# Copyright (C) 2019 CZ.NIC, z.s.p.o. <http://www.nic.cz>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import bottle
from foris.state import current_state
from foris.utils.translators import gettext_dummy as gettext, _
from .base import ConfigPageMixin
class NotificationsConfigPage(ConfigPageMixin):
slug = "notifications"
menu_order = 9
template = "config/notifications"
userfriendly_title = gettext("Notifications")
template_type = "jinja2"
def render(self, **kwargs):
notifications = current_state.backend.perform(
"router_notifications", "list", {"lang": current_state.language}
)["notifications"]
# show only non displayed notifications
kwargs["notifications"] = [e for e in notifications if not e["displayed"]]
return super(NotificationsConfigPage, self).render(**kwargs)
def _action_dismiss_notifications(self):
notification_ids = bottle.request.POST.getall("notification_ids[]")
response = current_state.backend.perform(
"router_notifications", "mark_as_displayed", {"ids": notification_ids})
return response["result"], notification_ids
def call_ajax_action(self, action):
if action == "dismiss-notifications":
bottle.response.set_header("Content-Type", "application/json")
res = self._action_dismiss_notifications()
if res[0]:
return {"success": True, "displayedIDs": res[1]}
else:
return {"success": False}
elif action == "list":
notifications = current_state.backend.perform(
"router_notifications", "list", {"lang": current_state.language}
)["notifications"]
return bottle.template(
"_notifications.html.j2",
notifications=[e for e in notifications if not e["displayed"]],
template_adapter=bottle.Jinja2Template,
)
raise ValueError("Unknown AJAX action.")
@classmethod
def get_menu_tag(cls):
return {
"show": True if current_state.notification_count else False,
"hint": _("Number of notifications"),
"text": "%d" % current_state.notification_count,
}
#
# Foris
# Copyright (C) 2019 CZ.NIC, z.s.p.o. <http://www.nic.cz>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from foris.config_handlers import misc
from foris.state import current_state
from foris.utils import messages
from foris.utils.translators import _
from .base import ConfigPageMixin
class PasswordConfigPage(ConfigPageMixin, misc.PasswordHandler):
slug = "password"
menu_order = 10
template = "config/password"
template_type = "jinja2"
def __init__(self, *args, **kwargs):
super(PasswordConfigPage, self).__init__(change=current_state.password_set, *args, **kwargs)
def save(self, *args, **kwargs):
result = super(PasswordConfigPage, self).save(no_messages=True, *args, **kwargs)
wrong_old_password = self.form.callback_results.get('wrong_old_password', False)
system_password_no_error = self.form.callback_results.get('system_password_no_error', None)
foris_password_no_error = self.form.callback_results.get('foris_password_no_error', None)
compromised = self.form.callback_results.get("compromised")
if compromised:
messages.error(
_(
"The password you've entered has been compromised. "
"It appears %(count)d times in '%(list)s' list."
) % dict(count=compromised['count'], list=compromised['list'])
)
return result
if wrong_old_password:
messages.error(_("Old password you entered was not valid."))
return result