__init__.py 11.6 KB
Newer Older
1
# Foris
2
# Copyright (C) 2019 CZ.NIC, z.s.p.o. <http://www.nic.cz>
3 4 5 6 7 8 9 10 11 12 13 14 15 16
#
# 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/>.

17 18
import logging

19
from bottle import Bottle, request, template, response
20 21
import bottle

22
from foris.common import login
23
from foris.utils.translators import _
24
from foris.utils import login_required, messages, is_safe_redirect
25
from foris.middleware.bottle_csrf import CSRFPlugin
Štěpán Henek's avatar
Štěpán Henek committed
26
from foris.utils.routing import reverse
27
from foris.state import current_state
Štěpán Henek's avatar
Štěpán Henek committed
28

29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
from .pages.base import ConfigPageMixin, JoinedPages  # TODO refactor plugins and remove this import

from .pages.notifications import NotificationsConfigPage
from .pages.password import PasswordConfigPage
from .pages.remote import RemoteConfigPage
from .pages.subordinates import SubordinatesJoinedPage
from .pages.guide import ProfileConfigPage, GuideFinishedPage
from .pages.networks import NetworksConfigPage
from .pages.wan import WanConfigPage
from .pages.time import TimeConfigPage
from .pages.dns import DNSConfigPage
from .pages.lan import LanConfigPage
from .pages.guest import GuestConfigPage
from .pages.wifi import WifiConfigPage
from .pages.maintenance import MaintenanceConfigPage
from .pages.updater import UpdaterConfigPage
from .pages.about import AboutConfigPage
46 47


48
logger = logging.getLogger(__name__)
49

50 51 52
config_pages = {
    e.slug: e for e in [
        NotificationsConfigPage,
Štěpán Henek's avatar
Štěpán Henek committed
53
        RemoteConfigPage,
54
        SubordinatesJoinedPage,
55 56 57 58 59 60 61 62 63 64 65
        PasswordConfigPage,
        ProfileConfigPage,
        NetworksConfigPage,
        WanConfigPage,
        TimeConfigPage,
        DNSConfigPage,
        LanConfigPage,
        GuestConfigPage,
        WifiConfigPage,
        MaintenanceConfigPage,
        UpdaterConfigPage,
66
        GuideFinishedPage,
67 68 69 70 71 72 73 74 75
        AboutConfigPage,
    ]
}


def get_config_pages():
    """ Returns sorted config pages
    """
    res = sorted(config_pages.values(), key=lambda e: (e.menu_order, e.slug))
76 77 78 79

    # sort subpages
    for page in res:
        page.subpages.sort(key=lambda e: (e.menu_order, e.slug))
80 81 82 83
    return res


def add_config_page(page_class):
Jan Čermák's avatar
Jan Čermák committed
84 85 86 87
    """Register config page in /config/ URL namespace.

    :param page_class: handler class
    """
88 89
    if page_class.slug is None:
        raise Exception("Page %s doesn't define a propper slug" % page_class)
90 91 92 93 94 95 96
    page_map = {k: v for k, v in config_pages.items()}

    for page in config_pages.values():
        for subpage in page.subpages:
            page_map[subpage.slug] = subpage

    if page_class.slug in page_map:
97
        raise Exception("Error when adding page %s slug '%s' is already used in %s" % (
98
            page_class, page_class.slug, page_map[page_class.slug]
99 100
        ))
    config_pages[page_class.slug] = page_class
Jan Čermák's avatar
Jan Čermák committed
101

102

103
def get_config_page(page_name):
104
    ConfigPage = config_pages.get(page_name, None)
105 106 107 108 109 110 111 112 113
    if ConfigPage:
        return ConfigPage

    # Try to iterate through subpages
    for page in config_pages.values():
        for subpage in page.subpages:
            if subpage.slug == page_name:
                return subpage
    raise bottle.HTTPError(404, "Unknown configuration page.")
114 115


116 117 118 119 120 121 122 123 124 125
def _redirect_to_default_location():

    next_page = "notifications"
    # by default redirect to current guide step
    if current_state.guide.enabled:
        next_page = current_state.guide.current if current_state.guide.current else next_page

    bottle.redirect(reverse("config_page", page_name=next_page))


126 127
@login_required
def index():
128
    _redirect_to_default_location()
129 130


131
@login_required
132
def config_page_get(page_name):
133 134 135 136
    # redirect in case that guide is not passed
    if current_state.guide.enabled and page_name not in current_state.guide.available_tabs:
        bottle.redirect(reverse("config_page", page_name=current_state.guide.current))

137
    bottle.SimpleTemplate.defaults['active_config_page_key'] = page_name
138
    bottle.Jinja2Template.defaults['active_config_page_key'] = page_name
139
    ConfigPage = get_config_page(page_name)
140 141 142 143 144

    # test if page is enabled otherwise redirect to default
    if not ConfigPage.is_enabled() or not ConfigPage.is_visible():
        _redirect_to_default_location()

145
    config_page = ConfigPage()
146
    return config_page.render(active_config_page_key=page_name)
147 148 149


@login_required
150
def config_page_post(page_name):
151
    bottle.SimpleTemplate.defaults['active_config_page_key'] = page_name
152
    bottle.Jinja2Template.defaults['active_config_page_key'] = page_name
153
    ConfigPage = get_config_page(page_name)
154
    config_page = ConfigPage(request.POST.decode())
155
    if request.is_xhr:
156
        if request.POST.pop("_update", None):
157
            # if update was requested, just render the page - otherwise handle actions as usual
158 159 160 161
            pass
        else:
            config_page.save()
        return config_page.render(is_xhr=True)
162
    try:
163
        if config_page.save():
164
            bottle.redirect(request.fullpath)
165 166
    except TypeError:
        # raised by Validator - could happen when the form is posted with wrong fields
167
        messages.error(_("Configuration could not be saved due to an internal error."))
168 169
        logger.exception("Error when saving form.")
    logger.warning("Form not saved.")
170
    return config_page.render(active_config_page_key=page_name)
171 172


173 174
@login_required
def config_action(page_name, action):
175
    bottle.SimpleTemplate.defaults['active_config_page'] = page_name
176
    bottle.Jinja2Template.defaults['active_config_page'] = page_name
177 178 179 180 181 182 183 184 185
    ConfigPage = get_config_page(page_name)
    config_page = ConfigPage()
    try:
        result = config_page.call_action(action)
        return result
    except ValueError:
        raise bottle.HTTPError(404, "Unknown action.")


186 187
@login_required
def config_action_post(page_name, action):
188
    bottle.SimpleTemplate.defaults['active_config_page_key'] = page_name
189
    bottle.Jinja2Template.defaults['active_config_page_key'] = page_name
190
    ConfigPage = get_config_page(page_name)
191
    config_page = ConfigPage(request.POST.decode())
192
    if request.is_xhr:
193
        if request.POST.pop("_update", None):
194 195
            # if update was requested, just render the page - otherwise handle actions as usual
            return config_page.render(is_xhr=True)
196 197 198 199
    # check if the button click wasn't any sub-action
    subaction = request.POST.pop("action", None)
    if subaction:
        return config_action_post(page_name, subaction)
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
    try:
        result = config_page.call_action(action)
        try:
            if not result:
                bottle.redirect(reverse("config_page", page_name=page_name))
        except TypeError:
            # raised by Validator - could happen when the form is posted with wrong fields
            messages.error(_("Configuration could not be saved due to an internal error."))
            logger.exception("Error when saving form.")
        logger.warning("Form not saved.")
        return result
    except ValueError:
        raise bottle.HTTPError(404, "Unknown action.")


215
@login_required
216
def config_ajax(page_name):
217
    bottle.SimpleTemplate.defaults['active_config_page_key'] = page_name
218
    bottle.Jinja2Template.defaults['active_config_page_key'] = page_name
219
    action = request.params.get("action")
220 221
    if not action:
        raise bottle.HTTPError(404, "AJAX action not specified.")
222 223
    ConfigPage = get_config_page(page_name)
    config_page = ConfigPage()
224
    try:
225
        result = config_page.call_ajax_action(action)
226 227
        return result
    except ValueError:
Bedřich Košata's avatar
Bedřich Košata committed
228
        raise bottle.HTTPError(404, "Unknown action.")
229 230


231 232 233 234 235 236 237 238 239 240
@login_required
def config_ajax_form(page_name, form_name):
    bottle.SimpleTemplate.defaults['active_config_page_key'] = page_name
    bottle.Jinja2Template.defaults['active_config_page_key'] = page_name
    ConfigPage = get_config_page(page_name)
    config_page = ConfigPage()
    if not request.is_xhr:
        raise bottle.HTTPError(400, "Should be ajax request")
    try:
        trigger = request.POST.pop("_update", None) is None
Štěpán Henek's avatar
Štěpán Henek committed
241
        hide = request.POST.pop("_hide", False)
242 243 244 245 246 247 248 249 250 251 252 253 254 255

        controller_id = request.POST.pop("_controller_id", None)
        form, response_handler = config_page.get_page_form(
            form_name, request.POST.decode(), controller_id
        )

        message = None
        if form.foris_form.validate() and trigger:
            form.foris_form.save()
            message = response_handler(form.foris_form.callback_results)

        return template(
            form.template_name,
            message=message,
Štěpán Henek's avatar
Štěpán Henek committed
256
            hide=hide,
257 258 259 260 261 262 263 264 265
            form=form.foris_form,
            ajax_form=form,
            template_adapter=bottle.Jinja2Template,
        )
    except (ValueError, KeyError):
        raise bottle.HTTPError(404, "Form not found.")
    raise bottle.HTTPError(404, "Form not found.")


Štěpán Henek's avatar
Štěpán Henek committed
266 267 268 269 270 271 272 273 274
def config_insecure(page_name, identifier):
    ConfigPage = get_config_page(page_name)
    config_page = ConfigPage(request.GET.decode())
    try:
        return config_page.call_insecure(identifier)
    except ValueError:
        raise bottle.HTTPError(404, "Unknown Insecure link")


275
def init_app():
276 277 278 279 280
    app = Bottle()
    app.install(CSRFPlugin())
    app.route("/", name="config_index", callback=index)
    app.route("/<page_name:re:.+>/ajax", name="config_ajax", method=("GET", "POST"),
              callback=config_ajax)
281 282
    app.route("/<page_name:re:.+>/ajax/form/<form_name:re:.+>", name="config_ajax_form", method=("POST"),
              callback=config_ajax_form)
283 284 285 286
    app.route("/<page_name:re:.+>/action/<action:re:.+>", method="POST",
              callback=config_action_post)
    app.route("/<page_name:re:.+>/action/<action:re:.+>", name="config_action",
              callback=config_action)
Štěpán Henek's avatar
Štěpán Henek committed
287 288
    app.route("/<page_name:re:.+>/insecure/<identifier:re:[0-9a-zA-Z-]+>",
              name="config_insecure", callback=config_insecure)
289 290 291 292
    app.route("/<page_name:re:.+>/", method="POST",
              callback=config_page_post)
    app.route("/<page_name:re:.+>/", name="config_page",
              callback=config_page_get)
293 294
    bottle.SimpleTemplate.defaults['get_config_pages'] = get_config_pages
    bottle.Jinja2Template.defaults['get_config_pages'] = get_config_pages
295
    return app
296 297 298 299 300 301 302 303 304


def login_redirect():
    next_url = bottle.request.GET.get("next")
    if next_url and is_safe_redirect(next_url, bottle.request.get_header('host')):
        bottle.redirect(next_url)
    bottle.redirect(reverse("config_index"))


305
@bottle.jinja2_view("index.html.j2")
306 307
def top_index():
    session = bottle.request.environ['foris.session']
308 309 310 311 312 313 314 315 316
    if bottle.request.method == 'POST':
        next = bottle.request.POST.get("next", None)
        login(next, session)
        # if login passes it will redirect to a proper page
        # otherwise it contains next parameter
        messages.error(_("The password you entered was not valid."))
        response.status = 403
    else:
        next = bottle.request.GET.get("next", None)
317
        if not current_state.password_set:  # auto login if no password is set
318 319 320 321
            if session.is_anonymous:
                session.recreate()
            session["user_authenticated"] = True
            session.save()
322

323 324
        if session.get("user_authenticated"):
            login_redirect()
325 326 327

    return dict(
        luci_path="//%(host)s/%(path)s"
328 329 330
        % {'host': bottle.request.get_header('host'), 'path': 'cgi-bin/luci'},
        next=next
    )