Esempio n. 1
0
def show_context_buttons(what, tardata):
    html.begin_context_buttons()
    if what == "check":
        site = html.request.var("site")
        host = html.request.var("host")
        service = html.request.var("service")

        host_url = html.makeuri([("view_name", "hoststatus"), ("host", host),
                                 ("site", site)],
                                filename="view.py")
        html.context_button(_("Host status"), host_url, "status")

        host_url = html.makeuri([("view_name", "service"), ("host", host),
                                 ("service", service), ("site", site)],
                                filename="view.py")
        html.context_button(_("Service status"), host_url, "status")

        download_url = html.makeuri([], filename="download_crash_report.py")
        html.context_button(_("Download"), download_url, "download")

    elif what == "gui":
        download_data_url = "data:application/octet-stream;base64,%s" % base64.b64encode(
            tardata)
        html.context_button(
            _("Download"), "javascript:cmk.crash_reporting.download('%s')" %
            download_data_url, "download")

    html.end_context_buttons()
Esempio n. 2
0
def show_log_list():
    title = _("All problematic logfiles")
    html.header(title, make_simple_page_breadcrumb(MegaMenuMonitoring, title))

    html.begin_context_buttons()
    html.context_button(
        _("Analyze Patterns"),
        "%swato.py?mode=pattern_editor" % html.request.var('master_url', ''),
        'analyze')
    ack_button()
    html.end_context_buttons()

    for site, host_name, logs in all_logs():
        if not logs:
            continue

        all_logs_empty = not any(
            [parse_file(site, host_name, file_name) for file_name in logs])

        if all_logs_empty:
            continue  # Logfile vanished

        html.h2(
            html.render_a(host_name,
                          href=html.makeuri([('site', site),
                                             ('host', host_name)])))
        list_logs(site, host_name, logs)
    html.footer()
Esempio n. 3
0
def page_add_snapin() -> None:
    if not config.user.may("general.configure_sidebar"):
        raise MKGeneralException(
            _("You are not allowed to change the sidebar."))

    html.header(_("Available snapins"))

    html.begin_context_buttons()
    CustomSnapins.context_button_list()
    html.end_context_buttons()

    used_snapins = _used_snapins()

    html.open_div(class_=["add_snapin"])
    for name, snapin_class in sorted(snapin_registry.items()):
        if name in used_snapins:
            continue
        if not config.user.may(snapin_class.permission_name()):
            continue  # not allowed for this user

        html.open_div(class_="snapinadder",
                      onmouseover="this.style.cursor=\'pointer\';",
                      onclick="window.top.cmk.sidebar.add_snapin('%s')" % name)

        html.open_div(class_=["snapin_preview"])
        html.div('', class_=["clickshield"])
        SidebarRenderer().render_snapin(
            UserSidebarSnapin.from_snapin_type_id(name))
        html.close_div()
        html.div(snapin_class.description(), class_=["description"])
        html.close_div()

    html.close_div()
    html.footer()
Esempio n. 4
0
def page_notify():
    if not config.user.may("general.notify"):
        raise MKAuthException(
            _("You are not allowed to use the notification module."))

    html.header(_('Notify Users'))

    html.begin_context_buttons()
    html.context_button(_("Users"), "wato.py?mode=users", "back")
    html.end_context_buttons()

    vs_notify = _vs_notify()

    if html.check_transaction():
        try:
            msg = vs_notify.from_html_vars("_notify")
            vs_notify.validate_value(msg, "_notify")
            _process_notify_message(msg)
        except MKUserError as e:
            html.user_error(e)

    html.begin_form("notify", method="POST")
    vs_notify.render_input_as_form("_notify", {})

    html.button("save", _("Send notification"))

    html.hidden_fields()
    html.end_form()
    html.footer()
Esempio n. 5
0
 def _show_context_buttons(self) -> None:
     rulebased_notifications = rulebased_notifications_enabled()
     if rulebased_notifications and config.user.may(
             'general.edit_notifications'):
         html.begin_context_buttons()
         url = "wato.py?mode=user_notifications_p"
         html.context_button(_("Notifications"), url, "notifications")
         html.end_context_buttons()
Esempio n. 6
0
def user_profile_async_replication_page():
    html.header(_('Replicate new User Profile'))

    html.begin_context_buttons()
    html.context_button(_('User Profile'), 'user_profile.py', 'back')
    html.end_context_buttons()

    sites = config.user.authorized_login_sites().keys()
    user_profile_async_replication_dialog(sites=sites)

    html.footer()
Esempio n. 7
0
    def _show_context_buttons(self, crash_info):
        html.begin_context_buttons()

        html.context_button(_("All crashes"), "view.py?view_name=crash_reports", "crash")

        self._crash_type_renderer(crash_info["crash_type"]).context_buttons(
            crash_info, self._site_id)

        download_url = html.makeuri([], filename="download_crash_report.py")
        html.context_button(_("Download"), download_url, "download")

        html.end_context_buttons()
Esempio n. 8
0
def page_werk():
    load_werks()
    werk_id = html.request.get_integer_input_mandatory("werk")
    if werk_id not in g_werks:
        raise MKUserError("werk", _("This werk does not exist."))
    werk = g_werks[werk_id]

    html.header(
        ("%s %s - %s") %
        (_("Werk"), render_werk_id(werk, with_link=False), werk["title"]))
    html.begin_context_buttons()
    back_url = html.makeuri([], filename="version.py",
                            delvars=["werk"])  # keeps filter settings
    html.context_button(_("Back"), back_url, "back")
    if werk["compatible"] == "incomp_unack" and may_acknowledge():
        ack_url = html.makeactionuri([("_werk_ack", werk["id"])],
                                     filename="version.py")
        html.context_button(_("Acknowledge"), ack_url, "werk_ack")
    html.end_context_buttons()

    html.open_table(class_=["data", "headerleft", "werks"])

    def werk_table_row(caption, content, css=None):
        html.open_tr()
        html.th(caption)
        html.open_td(class_=css)
        html.write(content)
        html.close_td()
        html.close_tr()

    translator = cmk.utils.werks.WerkTranslator()
    werk_table_row(_("ID"), render_werk_id(werk, with_link=False))
    werk_table_row(_("Title"), html.render_b(render_werk_title(werk)))
    werk_table_row(_("Component"), translator.component_of(werk))
    werk_table_row(_("Date"), render_werk_date(werk))
    werk_table_row(_("Checkmk Version"), werk["version"])
    werk_table_row(_("Level"),
                   translator.level_of(werk),
                   css="werklevel werklevel%d" % werk["level"])
    werk_table_row(_("Class"),
                   translator.class_of(werk),
                   css="werkclass werkclass%s" % werk["class"])
    werk_table_row(_("Compatibility"),
                   translator.compatibility_of(werk),
                   css="werkcomp werkcomp%s" % werk["compatible"])
    werk_table_row(_("Description"),
                   render_werk_description(werk),
                   css="nowiki")

    html.close_table()

    html.footer()
Esempio n. 9
0
def user_profile_async_replication_page() -> None:
    title = _('Replicate new user profile')
    breadcrumb = make_simple_page_breadcrumb(MegaMenuUser, title)
    html.header(title, breadcrumb)

    html.begin_context_buttons()
    html.context_button(_('User Profile'), 'user_profile.py', 'back')
    html.end_context_buttons()

    sites = list(config.user.authorized_login_sites().keys())
    user_profile_async_replication_dialog(sites=sites)

    html.footer()
Esempio n. 10
0
    def action(self):
        if html.request.var("_action") != "discard":
            return

        if not html.check_transaction():
            return

        if not self._may_discard_changes():
            return

        # TODO: Remove once new changes mechanism has been implemented
        # Now remove all currently pending changes by simply restoring the last automatically
        # taken snapshot. Then activate the configuration. This should revert all pending changes.
        file_to_restore = self._get_last_wato_snapshot_file()

        if not file_to_restore:
            raise MKUserError(None,
                              _('There is no WATO snapshot to be restored.'))

        msg = _("Discarded pending changes (Restored %s)") % file_to_restore

        # All sites and domains can be affected by a restore: Better restart everything.
        watolib.add_change("changes-discarded",
                           msg,
                           domains=watolib.ABCConfigDomain.enabled_domains(),
                           need_restart=True)

        self._extract_snapshot(file_to_restore)
        cmk.gui.watolib.activate_changes.execute_activate_changes(
            [d.ident for d in watolib.ABCConfigDomain.enabled_domains()])

        for site_id in cmk.gui.watolib.changes.activation_sites():
            self.confirm_site_changes(site_id)

        html.header(self.title(),
                    show_body_start=display_options.enabled(display_options.H),
                    show_top_heading=display_options.enabled(
                        display_options.T))
        html.open_div(class_="wato")

        html.begin_context_buttons()
        home_button()
        html.end_context_buttons()

        html.show_message(_("Successfully discarded all pending changes."))
        html.javascript("hide_changes_buttons();")
        html.footer()

        return False
Esempio n. 11
0
def show_host_log_list(site, host_name):
    html.header(_("Logfiles of host %s") % host_name)

    html.begin_context_buttons()
    html.context_button(_("Services"), services_url(site, host_name), 'services')
    button_all_logfiles()
    html.context_button(_("Analyze host patterns"), analyse_url(site, host_name), 'analyze')
    ack_button(site, host_name)
    html.end_context_buttons()

    html.open_table(class_=["data"])
    list_logs(site, host_name, logfiles_of_host(site, host_name))
    html.close_table()

    html.footer()
Esempio n. 12
0
    def page(self):
        html.header(_("%s: Download agent output") % self._request.host.name())

        html.begin_context_buttons()
        if self._back_url:
            html.context_button(_("Back"), self._back_url, "back")
        html.end_context_buttons()

        self._action()

        if html.request.has_var("_start"):
            self._start_fetch()
        self._show_status()

        html.footer()
Esempio n. 13
0
    def page(self) -> None:
        title = self._title()
        html.header(title, self._breadcrumb(title))

        html.begin_context_buttons()
        if self._back_url:
            html.context_button(_("Back"), self._back_url, "back")
        html.end_context_buttons()

        self._action()

        if html.request.has_var("_start"):
            self._start_fetch()
        self._show_status()

        html.footer()
Esempio n. 14
0
def render_unacknowleged_werks():
    werks = unacknowledged_incompatible_werks()
    if werks and may_acknowledge():
        html.begin_context_buttons()
        html.context_button(_("Acknowledge all"), html.makeactionuri([("_ack_all", "1")]),
                            "werk_ack")
        html.end_context_buttons()

    if werks and not html.request.has_var("show_unack"):
        html.open_div(class_=["warning"])
        html.write_text(
            _("<b>Warning:</b> There are %d unacknowledged incompatible werks:") % len(werks))
        html.br()
        html.br()
        html.a(_("Show unacknowledged incompatible werks"),
               href=html.makeuri_contextless([("show_unack", "1"), ("wo_compatibility", "3")]))
        html.close_div()
Esempio n. 15
0
    def show(self):
        # type: () -> None
        if not config.user.may("general.configure_sidebar"):
            raise MKGeneralException(_("You are not allowed to change the sidebar."))

        html.header(_("Available snapins"))

        html.begin_context_buttons()
        CustomSnapins.context_button_list()
        html.end_context_buttons()

        addname = html.request.var("name")
        if addname in snapin_registry and addname not in self._used_snapins(
        ) and html.check_transaction():
            self._user_config.add_snapin(UserSidebarSnapin.from_snapin_type_id(addname))
            self._user_config.save()
            html.reload_sidebar()

        self._show_builtin_snapins()
Esempio n. 16
0
def edit_annotation():
    site_id = html.request.var("anno_site") or ""
    hostname = html.request.get_str_input_mandatory("anno_host")
    service = html.request.var("anno_service") or None
    fromtime = html.request.get_float_input_mandatory("anno_from")
    untiltime = html.request.get_float_input_mandatory("anno_until")
    site_host_svc = (site_id, hostname, service)

    # Find existing annotation with this specification
    annotations = availability.load_annotations()
    annotation = availability.find_annotation(annotations, site_host_svc,
                                              fromtime, untiltime)

    if annotation:
        value = annotation.copy()
    else:
        value = {
            "from": fromtime,
            "until": untiltime,
            "text": "",
        }

    value["host"] = hostname
    value["service"] = service
    value["site"] = site_id

    if html.check_transaction():
        try:
            vs = _vs_annotation()
            value = vs.from_html_vars("_editanno")
            vs.validate_value(value, "_editanno")

            site_host_svc = (value["site"], value["host"], value["service"])
            del value["site"]
            del value["host"]
            value["date"] = time.time()
            value["author"] = config.user.id
            availability.update_annotations(site_host_svc,
                                            value,
                                            replace_existing=annotation)
            html.request.del_var("filled_in")
            return False
        except MKUserError as e:
            html.user_error(e)

    title = _("Edit annotation of ") + hostname
    if service:
        title += "/" + service

    html.body_start(title)
    html.top_heading(title)

    html.begin_context_buttons()
    html.context_button(_("Abort"), html.makeuri([("anno_host", "")]), "abort")
    html.end_context_buttons()

    html.begin_form("editanno", method="GET")
    _vs_annotation().render_input_as_form("_editanno", value)

    html.button("save", _("Save"))

    html.hidden_fields()
    html.end_form()

    html.bottom_footer()
    html.body_end()
    return True
Esempio n. 17
0
def render_bi_availability(title, aggr_rows):
    # type: (Text, Rows) -> None
    config.user.need_permission("general.see_availability")

    av_mode = html.request.get_ascii_input_mandatory("av_mode", "availability")
    avoptions = get_availability_options_from_url("bi")
    if av_mode == "timeline":
        title = _("Timeline of") + " " + title
    else:
        title = _("Availability of") + " " + title

    if html.output_format != "csv_export":
        html.body_start(title)
        html.top_heading(title)
        html.begin_context_buttons()
        html.toggle_button("avoptions", False, "painteroptions",
                           _("Configure details of the report"))
        html.context_button(_("Status View"),
                            html.makeuri([("mode", "status")]), "status")
        if config.reporting_available() and config.user.may(
                "general.reporting"):
            html.context_button(_("Export as PDF"),
                                html.makeuri([], filename="report_instant.py"),
                                "report")
        if config.user.may("general.csv_export"):
            html.context_button(
                _("Export as CSV"),
                html.makeuri([("output_format", "csv_export")]),
                "download_csv")

        if av_mode == "timeline":
            html.context_button(_("Availability"),
                                html.makeuri([("av_mode", "availability")]),
                                "availability")

        elif len(aggr_rows) == 1:
            aggr_name = aggr_rows[0]["aggr_name"]
            aggr_group = aggr_rows[0]["aggr_group"]
            timeline_url = html.makeuri([("av_mode", "timeline"),
                                         ("av_aggr_name", aggr_name),
                                         ("av_aggr_group", aggr_group)])
            html.context_button(_("Timeline"), timeline_url, "timeline")
        html.end_context_buttons()

        avoptions = render_availability_options("bi")

    if not html.has_user_errors():
        logrow_limit = avoptions["logrow_limit"]
        if logrow_limit == 0:
            livestatus_limit = None
        else:
            livestatus_limit = (len(aggr_rows) * logrow_limit)

        spans = []  # type: List[AVSpan]

        # iterate all aggregation rows
        timewarpcode = HTML()
        timewarp = html.request.get_integer_input("timewarp")

        has_reached_logrow_limit = False
        timeline_containers, fetched_rows = availability.get_timeline_containers(
            aggr_rows, avoptions, timewarp,
            livestatus_limit + 1 if livestatus_limit is not None else None)
        if livestatus_limit and fetched_rows > livestatus_limit:
            has_reached_logrow_limit = True

        for timeline_container in timeline_containers:
            tree = timeline_container.aggr_tree
            these_spans = timeline_container.timeline
            timewarp_tree_state = timeline_container.timewarp_state

            spans += these_spans

            # render selected time warp for the corresponding aggregation row (should be matched by only one)
            if timewarp and timewarp_tree_state:
                state, assumed_state, node, _subtrees = timewarp_tree_state
                eff_state = state
                if assumed_state is not None:
                    eff_state = assumed_state
                row = {
                    "aggr_tree": tree,
                    "aggr_treestate": timewarp_tree_state,
                    "aggr_state": state,  # state disregarding assumptions
                    "aggr_assumed_state":
                    assumed_state,  # is None, if there are no assumptions
                    "aggr_effective_state":
                    eff_state,  # is assumed_state, if there are assumptions, else real state
                    "aggr_name": node["title"],
                    "aggr_output": eff_state["output"],
                    "aggr_hosts": node["reqhosts"],
                    "aggr_function": node["func"],
                    "aggr_group": html.request.var("aggr_group"),
                }

                renderer = bi.FoldableTreeRendererTree(
                    row,
                    omit_root=False,
                    expansion_level=config.user.bi_expansion_level,
                    only_problems=False,
                    lazy=False)
                tdclass, htmlcode = renderer.css_class(), renderer.render()

                with html.plugged():
                    # TODO: SOMETHING IS WRONG IN HERE (used to be the same situation in original code!)
                    # FIXME: WHAT is wrong in here??

                    html.open_h3()
                    # render icons for back and forth
                    button_back_shown = False
                    button_forth_shown = False
                    if int(these_spans[0]["from"]) == timewarp:
                        html.disabled_icon_button("back_off")
                        button_back_shown = True

                    previous_span = None
                    for span in these_spans:
                        if not button_back_shown and int(
                                span["from"]
                        ) == timewarp and previous_span is not None:
                            html.icon_button(
                                html.makeuri([("timewarp",
                                               str(int(previous_span["from"])))
                                              ]), _("Jump one phase back"),
                                "back")
                            button_back_shown = True
                        # Multiple followup spans can have the same "from" time
                        # We only show one forth-arrow with an actual time difference
                        elif not button_forth_shown and previous_span and int(
                                previous_span["from"]) == timewarp and int(
                                    span["from"]) != timewarp:
                            html.icon_button(
                                html.makeuri([("timewarp",
                                               str(int(span["from"])))]),
                                _("Jump one phase forth"), "forth")
                            button_forth_shown = True
                        previous_span = span
                    if not button_forth_shown:
                        html.disabled_icon_button("forth_off")

                    html.write_text(" &nbsp; ")
                    html.icon_button(html.makeuri([("timewarp", "")]),
                                     _("Close Timewarp"), "closetimewarp")
                    html.write_text("%s %s" %
                                    (_("Timewarp to "),
                                     time.strftime("%Y-%m-%d %H:%M:%S",
                                                   time.localtime(timewarp))))
                    html.close_h3()

                    html.open_table(class_=["data", "table", "timewarp"])
                    html.open_tr(class_=["data", "odd0"])
                    html.open_td(class_=tdclass)
                    html.write_html(htmlcode)
                    html.close_td()
                    html.close_tr()
                    html.close_table()

                    timewarpcode += html.drain()

        # Note: 'spans_by_object' returns two arguments which are used by
        # all availability views but not by BI. There we have to take
        # only complete aggregations
        av_rawdata = availability.spans_by_object(spans, None)[0]
        av_data = availability.compute_availability("bi", av_rawdata,
                                                    avoptions)

        # If we abolish the limit we have to fetch the data again
        # with changed logrow_limit = 0, which means no limit
        if has_reached_logrow_limit:
            text = _(
                "Your query matched more than %d log entries. "
                "<b>Note:</b> The shown data does not necessarily reflect the "
                "matched entries and the result might be incomplete. "
            ) % avoptions["logrow_limit"]
            text += html.render_a(_('Repeat query without limit.'),
                                  html.makeuri([("_unset_logrow_limit", "1")]))
            html.show_warning(text)

        if html.output_format == "csv_export" and config.user.may(
                "general.csv_export"):
            _output_csv("bi", av_mode, av_data, avoptions)
            return

        html.write(timewarpcode)
        do_render_availability("bi", av_rawdata, av_data, av_mode, None,
                               avoptions)

    html.bottom_footer()
    html.body_end()
Esempio n. 18
0
def render_availability_page(view, filterheaders):
    # type: (View, FilterHeaders) -> None
    config.user.need_permission("general.see_availability")

    if handle_edit_annotations():
        return

    # We make reports about hosts, services or BI aggregates
    if "service" in view.datasource.infos:
        what = "service"
    elif "aggr_name" in view.datasource.infos:
        what = "bi"
    else:
        what = "host"

    avoptions = get_availability_options_from_url(what)
    time_range, range_title = avoptions["range"]

    # We have two display modes:
    # - Show availability table (stats) "table"
    # - Show timeline                   "timeline"
    # --> controlled by URL variable "av_mode"
    av_mode = html.request.get_ascii_input_mandatory("av_mode", "table")

    if av_mode == "timeline":
        title = _("Availability Timeline")
    else:
        title = _("Availability")

    # This is combined with the object selection
    # - Show all objects
    # - Show one specific object
    # --> controlled by URL variables "av_site", "av_host" and "av_service"
    # --> controlled by "av_aggr" in case of BI aggregate
    title += " - "
    av_object = None  # type: AVObjectSpec
    if html.request.var("av_host"):
        av_object = (html.request.get_str_input_mandatory("av_site"),
                     html.request.get_str_input_mandatory("av_host"),
                     html.request.get_unicode_input_mandatory("av_service"))
        title += av_object[1]
        if av_object[2]:
            title += " - " + av_object[2]
    elif html.request.var("av_aggr"):
        av_object = (None, None,
                     html.request.get_unicode_input_mandatory("av_aggr"))
        title += av_object[2]
    else:
        title += view_title(view.spec)

    # Deletion must take place before computation, since it affects the outcome
    with html.plugged():
        handle_delete_annotations()
        confirmation_html_code = html.drain()

    # Now compute all data, we need this also for CSV export
    if not html.has_user_errors():
        include_long_output = av_mode == "timeline" \
                and "timeline_long_output" in avoptions["labelling"]
        av_rawdata, has_reached_logrow_limit = availability.get_availability_rawdata(
            what,
            view.context,
            filterheaders,
            view.only_sites,
            av_object=av_object,
            include_output=av_mode == "timeline",
            include_long_output=include_long_output,
            avoptions=avoptions)
        av_data = availability.compute_availability(what, av_rawdata,
                                                    avoptions)

    # Do CSV ouput
    if html.output_format == "csv_export" and config.user.may(
            "general.csv_export"):
        _output_csv(what, av_mode, av_data, avoptions)
        return

    title += " - " + range_title

    if display_options.enabled(display_options.H):
        html.body_start(title, force=True)

    if display_options.enabled(display_options.T):
        html.top_heading(title)

    html.write(confirmation_html_code)

    # Remove variables for editing annotations, otherwise they will make it into the uris
    html.request.del_vars("anno_")
    if html.request.var("filled_in") == "editanno":
        html.request.del_var("filled_in")

    if display_options.enabled(display_options.B):
        html.begin_context_buttons()
        html.toggle_button(
            "avoptions",
            html.has_user_errors(),
            "painteroptions",
            _("Configure details of the report"),
        )
        html.context_button(
            _("Status View"),
            html.makeuri([("mode", "status")]),
            "status",
        )
        if config.reporting_available() and config.user.may(
                "general.reporting"):
            html.context_button(
                _("Export as PDF"),
                html.makeuri([], filename="report_instant.py"),
                "report",
            )

        if config.user.may("general.csv_export"):
            html.context_button(
                _("Export as CSV"),
                html.makeuri([("output_format", "csv_export")]),
                "download_csv",
            )

        if av_mode == "timeline" or av_object:
            html.context_button(
                _("Availability"),
                html.makeuri([("av_mode", "availability"), ("av_host", ""),
                              ("av_aggr", "")]),
                "availability",
            )
        elif not av_object:
            html.context_button(
                _("Timeline"),
                html.makeuri([("av_mode", "timeline")]),
                "timeline",
            )
        elif av_mode == "timeline" and what != "bi":
            history_url = availability.history_url_of(av_object, time_range)
            html.context_button(
                _("History"),
                history_url,
                "history",
            )

        html.end_context_buttons()

    # Render the avoptions again to get the HTML code, because the HTML vars have changed
    # above (anno_ and editanno_ has been removed, which must not be part of the form
    avoptions = render_availability_options(what)

    if not html.has_user_errors():
        # If we abolish the limit we have to fetch the data again
        # with changed logrow_limit = 0, which means no limit
        if has_reached_logrow_limit:
            text = _(
                "Your query matched more than %d log entries. "
                "<b>Note:</b> The number of shown rows does not necessarily reflect the "
                "matched entries and the result might be incomplete. "
            ) % avoptions["logrow_limit"]
            text += html.render_a(
                _('Repeat query without limit.'),
                html.makeuri([("_unset_logrow_limit", "1"),
                              ("avo_logrow_limit", 0)]))
            html.show_warning(text)

        do_render_availability(what, av_rawdata, av_data, av_mode, av_object,
                               avoptions)

    if display_options.enabled(display_options.Z):
        html.bottom_footer()

    if display_options.enabled(display_options.H):
        html.body_end()
Esempio n. 19
0
def _wato_page_handler(current_mode: str, mode_permissions: List[PermissionName],
                       mode_class: Type[WatoMode]) -> None:
    try:
        init_wato_datastructures(with_wato_lock=not html.is_transaction())
    except Exception:
        # Snapshot must work in any case
        if current_mode == 'snapshot':
            pass
        else:
            raise

    # Check general permission for this mode
    if mode_permissions is not None and not config.user.may("wato.seeall"):
        _ensure_mode_permissions(mode_permissions)

    mode = mode_class()

    # Do actions (might switch mode)
    action_message: Optional[str] = None
    if html.is_transaction():
        try:
            config.user.need_permission("wato.edit")

            # Even if the user has seen this mode because auf "seeall",
            # he needs an explicit access permission for doing changes:
            if config.user.may("wato.seeall"):
                if mode_permissions:
                    _ensure_mode_permissions(mode_permissions)

            if cmk.gui.watolib.read_only.is_enabled(
            ) and not cmk.gui.watolib.read_only.may_override():
                raise MKUserError(None, cmk.gui.watolib.read_only.message())

            result = mode.action()
            if isinstance(result, tuple):
                newmode, action_message = result
            else:
                newmode = result

            # We assume something has been modified and increase the config generation ID by one.
            update_config_generation()

            # If newmode is False, then we shall immediately abort.
            # This is e.g. the case, if the page outputted non-HTML
            # data, such as a tarball (in the export function). We must
            # be sure not to output *any* further data in that case.
            if newmode is False:
                return

            # if newmode is not None, then the mode has been changed
            if newmode is not None:
                assert not isinstance(newmode, bool)
                if newmode == "":  # no further information: configuration dialog, etc.
                    if action_message:
                        html.show_message(action_message)
                        wato_html_footer()
                    return
                mode_permissions, mode_class = _get_mode_permission_and_class(newmode)
                current_mode = newmode
                mode = mode_class()
                html.request.set_var("mode", newmode)  # will be used by makeuri

                # Check general permissions for the new mode
                if mode_permissions is not None and not config.user.may("wato.seeall"):
                    for pname in mode_permissions:
                        if '.' not in pname:
                            pname = "wato." + pname
                        config.user.need_permission(pname)

        except MKUserError as e:
            action_message = "%s" % e
            html.add_user_error(e.varname, action_message)

        except MKAuthException as e:
            reason = e.args[0]
            action_message = reason
            html.add_user_error(None, reason)

    breadcrumb = make_main_menu_breadcrumb(mode.main_menu()) + mode.breadcrumb()
    page_menu = mode.page_menu(breadcrumb)
    wato_html_head(title=mode.title(),
                   breadcrumb=breadcrumb,
                   page_menu=page_menu,
                   show_body_start=display_options.enabled(display_options.H),
                   show_top_heading=display_options.enabled(display_options.T))

    # As long as we migrate the buttons to the page menu keep the buttons as fallback
    if display_options.enabled(display_options.B):
        # Show contexts buttons
        html.begin_context_buttons()
        mode.buttons()
        html.end_context_buttons()

    if not html.is_transaction() or (cmk.gui.watolib.read_only.is_enabled() and
                                     cmk.gui.watolib.read_only.may_override()):
        _show_read_only_warning()

    # Show outcome of action
    if html.has_user_errors():
        html.show_user_errors()
    elif action_message:
        html.show_message(action_message)

    # Show content
    mode.handle_page()

    if is_sidebar_reload_needed():
        html.reload_sidebar()

    if config.wato_use_git and html.is_transaction():
        do_git_commit()

    wato_html_footer(show_footer=display_options.enabled(display_options.Z),
                     show_body_end=display_options.enabled(display_options.H))
Esempio n. 20
0
    def page_edit(cls):
        back_url = html.get_url_input("back", cls.list_url())

        cls.load()
        cls.need_overriding_permission("edit")

        # Three possible modes:
        # "create" -> create completely new page
        # "clone"  -> like new, but prefill form with values from existing page
        # "edit"   -> edit existing page
        mode = html.request.var('mode', 'edit')
        if mode == "create":
            title = cls.phrase("create")
            page_dict = {
                "name": cls.default_name(),
                "topic": cls.default_topic(),
            }
        else:
            # Load existing page. visual from disk - and create a copy if 'load_user' is set
            page_name = html.request.var("load_name")
            if mode == "edit":
                title = cls.phrase("edit")

                owner_user_id = UserId(
                    html.request.get_unicode_input_mandatory("owner", config.user.id))
                if owner_user_id == config.user.id:
                    page = cls.find_my_page(page_name)
                else:
                    page = cls.find_foreign_page(owner_user_id, page_name)

                if page is None:
                    raise MKUserError(None,
                                      _("The requested %s does not exist") % cls.phrase("title"))

                # TODO FIXME: Looks like a hack
                cls.remove_instance((owner_user_id, page_name))  # will be added later again
            else:  # clone
                title = cls.phrase("clone")
                load_user = html.request.get_unicode_input(
                    "load_user")  # FIXME: Change varname to "owner"

                try:
                    page = cls.instance((load_user, page_name))
                except KeyError:
                    raise MKUserError(None,
                                      _("The requested %s does not exist") % cls.phrase("title"))
            page_dict = page.internal_representation()

        html.header(title)
        html.begin_context_buttons()
        html.context_button(_("Back"), back_url, "back")
        html.end_context_buttons()

        parameters, keys_by_topic = cls._collect_parameters(mode)
        vs = Dictionary(
            title=_("General Properties"),
            render='form',
            optional_keys=False,
            elements=parameters,
            headers=keys_by_topic,
        )

        def validate(page_dict):
            owner_user_id = UserId(html.request.get_unicode_input_mandatory(
                "owner", config.user.id))
            page_name = page_dict["name"]
            if owner_user_id == config.user.id:
                page = cls.find_my_page(page_name)
            else:
                page = cls.find_foreign_page(owner_user_id, page_name)
            if page:
                raise MKUserError(
                    "_p_name",
                    _("You already have an element with the ID <b>%s</b>") % page_dict["name"])

        new_page_dict = forms.edit_valuespec(vs,
                                             page_dict,
                                             validate=validate,
                                             focus="_p_title",
                                             method="POST")
        if new_page_dict is not None:
            # Take over keys from previous value that are specific to the page type
            # and not edited here.
            if mode in ("edit", "clone"):
                for key, value in page_dict.items():
                    new_page_dict.setdefault(key, value)

            owner = UserId(html.request.get_unicode_input_mandatory("owner", config.user.id))
            new_page_dict["owner"] = owner
            new_page = cls(new_page_dict)

            cls.add_page(new_page)
            cls.save_user_instances(owner)
            if mode == "create":
                redirect_url = new_page.after_create_url() or back_url
            else:
                redirect_url = back_url

            html.immediate_browser_redirect(0.5, redirect_url)
            html.show_message(_('Your changes haven been saved.'))
            # Reload sidebar.TODO: This code logically belongs to PageRenderer. How
            # can we simply move it there?
            # TODO: This is not true for all cases. e.g. the BookmarkList is not
            # of type PageRenderer but has a dedicated sidebar snapin. Maybe
            # the best option would be to make a dedicated method to decide whether
            # or not to reload the sidebar.
            if new_page_dict.get("hidden") in [ None, False ] \
               or new_page_dict.get("hidden") != page_dict.get("hidden"):
                html.reload_sidebar()

        else:
            html.show_localization_hint()

        html.footer()
        return
Esempio n. 21
0
    def page_list(cls):
        cls.load()

        # custom_columns = []
        # render_custom_buttons = None
        # render_custom_columns = None
        # render_custom_context_buttons = None
        # check_deletable_handler = None

        cls.need_overriding_permission("edit")

        html.header(cls.phrase("title_plural"))
        html.begin_context_buttons()
        html.context_button(cls.phrase("new"), cls.create_url(), "new_" + cls.type_name())

        # TODO: Remove this legacy code as soon as views, dashboards and reports have been
        # moved to pagetypes.py
        html.context_button(_("Views"), "edit_views.py", "view")
        html.context_button(_("Dashboards"), "edit_dashboards.py", "dashboard")

        def has_reporting():
            try:
                # The suppression below is OK, we just want to check if the module is there.
                import cmk.gui.cee.reporting  # noqa: F401 # pylint: disable=unused-variable,redefined-outer-name
                return True
            except ImportError:
                return False

        if has_reporting():
            html.context_button(_("Reports"), "edit_reports.py", "report")

        # ## if render_custom_context_buttons:
        # ##     render_custom_context_buttons()

        for other_type_name, other_pagetype in page_types.items():
            if cls.type_name() != other_type_name:
                html.context_button(
                    other_pagetype.phrase("title_plural").title(), '%ss.py' % other_type_name,
                    other_type_name)
        html.end_context_buttons()

        # Deletion
        delname = html.request.var("_delete")
        if delname and html.transaction_valid():
            owner = UserId(html.request.get_unicode_input_mandatory('_owner', config.user.id))

            try:
                instance = cls.instance((owner, delname))
            except KeyError:
                raise MKUserError(
                    "_delete",
                    _("The %s you are trying to delete "
                      "does not exist.") % cls.phrase("title"))

            if not instance.may_delete():
                raise MKUserError("_delete", _("You are not permitted to perform this action."))

            try:
                if owner != config.user.id:
                    owned_by = _(" (owned by %s)") % owner
                else:
                    owned_by = ""
                c = html.confirm(
                    _("Please confirm the deletion of \"%s\"%s.") % (instance.title(), owned_by))
                if c:
                    cls.remove_instance((owner, delname))
                    cls.save_user_instances(owner)
                    html.reload_sidebar()
                elif c is False:
                    html.footer()
                    return
            except MKUserError as e:
                html.user_error(e)

        # Bulk delete
        if html.request.var("_bulk_delete_my") and html.transaction_valid():
            if cls._bulk_delete_after_confirm("my") is False:
                html.footer()
                return

        elif html.request.var("_bulk_delete_foreign") and html.transaction_valid():
            if cls._bulk_delete_after_confirm("foreign") is False:
                html.footer()
                return

        my_instances, foreign_instances, builtin_instances = cls.get_instances()
        for what, title, instances in [
            ("my", _('Customized'), my_instances),
            ("foreign", _('Owned by other users'), foreign_instances),
            ("builtin", _('Builtin'), builtin_instances),
        ]:
            if not instances:
                continue

            html.open_h3()
            html.write(title)
            html.close_h3()

            if what != "builtin":
                html.begin_form("bulk_delete_%s" % what, method="POST")

            with table_element(limit=None) as table:
                for instance in instances:
                    table.row()

                    if what != "builtin" and instance.may_delete():
                        table.cell(html.render_input(
                            "_toggle_group",
                            type_="button",
                            class_="checkgroup",
                            onclick="cmk.selection.toggle_all_rows(this.form);",
                            value='X'),
                                   sortable=False,
                                   css="checkbox")
                        html.checkbox("_c_%s+%s+%s" % (what, instance.owner(), instance.name()))

                    # Actions
                    table.cell(_('Actions'), css='buttons visuals')

                    # View
                    if isinstance(instance, PageRenderer):
                        html.icon_button(instance.page_url(), _("View"), "new_" + cls.type_name())

                    # Clone / Customize
                    html.icon_button(instance.clone_url(), _("Create a customized copy of this"),
                                     "clone")

                    # Delete
                    if instance.may_delete():
                        html.icon_button(instance.delete_url(), _("Delete!"), "delete")

                    # Edit
                    if instance.may_edit():
                        html.icon_button(instance.edit_url(), _("Edit"), "edit")

                    cls.custom_list_buttons(instance)

                    # Internal ID of instance (we call that 'name')
                    table.cell(_('ID'), instance.name(), css="narrow")

                    # Title
                    table.cell(_('Title'))
                    html.write_text(instance.render_title())
                    html.help(_u(instance.description()))

                    # Custom columns specific to that page type
                    instance.render_extra_columns(table)

                    # ## for title, renderer in custom_columns:
                    # ##     table.cell(title, renderer(visual))

                    # Owner
                    if instance.is_builtin():
                        ownertxt = html.render_i(_("builtin"))
                    else:
                        ownertxt = instance.owner()
                    table.cell(_('Owner'), ownertxt)
                    table.cell(_('Public'), _("yes") if instance.is_public() else _("no"))
                    table.cell(_('Hidden'), _("yes") if instance.is_hidden() else _("no"))

                    # FIXME: WTF?!?
                    # TODO: Haeeh? Another custom columns
                    # ## if render_custom_columns:
                    # ##     render_custom_columns(visual_name, visual)

            if what != "builtin":
                html.button("_bulk_delete_%s" % what,
                            _("Bulk delete"),
                            "submit",
                            style="margin-top:10px")
                html.hidden_fields()
                html.end_form()

        html.footer()
        return
Esempio n. 22
0
def _show_page_user_profile(change_pw):
    start_async_replication = False

    if not config.user.id:
        raise MKUserError(None, _('Not logged in.'))

    if not config.user.may('general.edit_profile') and not config.user.may(
            'general.change_password'):
        raise MKAuthException(
            _("You are not allowed to edit your user profile."))

    if not config.wato_enabled:
        raise MKAuthException(
            _('User profiles can not be edited (WATO is disabled).'))

    success = None
    if html.request.has_var('_save') and html.check_transaction():
        users = userdb.load_users(lock=True)

        try:
            # Profile edit (user options like language etc.)
            if config.user.may('general.edit_profile'):
                if not change_pw:
                    set_lang = html.get_checkbox('_set_lang')
                    language = html.request.var('language')
                    # Set the users language if requested
                    if set_lang:
                        if language == '':
                            language = None
                        # Set custom language
                        users[config.user.id]['language'] = language
                        config.user.language = language
                        html.set_language_cookie(language)

                    else:
                        # Remove the customized language
                        if 'language' in users[config.user.id]:
                            del users[config.user.id]['language']
                        config.user.reset_language()

                    # load the new language
                    cmk.gui.i18n.localize(config.user.language)

                    user = users.get(config.user.id)
                    if config.user.may('general.edit_notifications'
                                       ) and user.get("notifications_enabled"):
                        value = forms.get_input(
                            watolib.get_vs_flexible_notifications(),
                            "notification_method")
                        users[config.user.id]["notification_method"] = value

                    # Custom attributes
                    if config.user.may('general.edit_user_attributes'):
                        for name, attr in userdb.get_user_attributes():
                            if attr.user_editable():
                                if not attr.permission() or config.user.may(
                                        attr.permission()):
                                    vs = attr.valuespec()
                                    value = vs.from_html_vars('ua_' + name)
                                    vs.validate_value(value, "ua_" + name)
                                    users[config.user.id][name] = value

            # Change the password if requested
            password_changed = False
            if config.user.may('general.change_password'):
                cur_password = html.request.var('cur_password')
                password = html.request.var('password')
                password2 = html.request.var('password2', '')

                if change_pw:
                    # Force change pw mode
                    if not cur_password:
                        raise MKUserError(
                            "cur_password",
                            _("You need to provide your current password."))
                    if not password:
                        raise MKUserError(
                            "password", _("You need to change your password."))
                    if cur_password == password:
                        raise MKUserError(
                            "password",
                            _("The new password must differ from your current one."
                              ))

                if cur_password and password:
                    if userdb.hook_login(config.user.id,
                                         cur_password) is False:
                        raise MKUserError("cur_password",
                                          _("Your old password is wrong."))
                    if password2 and password != password2:
                        raise MKUserError(
                            "password2",
                            _("The both new passwords do not match."))

                    watolib.verify_password_policy(password)
                    users[config.user.id]['password'] = hash_password(password)
                    users[config.user.id]['last_pw_change'] = int(time.time())

                    if change_pw:
                        # Has been changed, remove enforcement flag
                        del users[config.user.id]['enforce_pw_change']

                    # Increase serial to invalidate old cookies
                    if 'serial' not in users[config.user.id]:
                        users[config.user.id]['serial'] = 1
                    else:
                        users[config.user.id]['serial'] += 1

                    password_changed = True

            # Now, if in distributed environment where users can login to remote sites,
            # set the trigger for pushing the new auth information to the slave sites
            # asynchronous
            if config.user.authorized_login_sites():
                start_async_replication = True

            userdb.save_users(users)

            if password_changed:
                # Set the new cookie to prevent logout for the current user
                login.set_auth_cookie(config.user.id)

            success = True
        except MKUserError as e:
            html.add_user_error(e.varname, e)
    else:
        users = userdb.load_users()

    watolib.init_wato_datastructures(with_wato_lock=True)

    # When in distributed setup, display the replication dialog instead of the normal
    # profile edit dialog after changing the password.
    if start_async_replication:
        user_profile_async_replication_page()
        return

    if change_pw:
        title = _("Change Password")
    else:
        title = _("Edit User Profile")

    html.header(title)

    # Rule based notifications: The user currently cannot simply call the according
    # WATO module due to WATO permission issues. So we cannot show this button
    # right now.
    if not change_pw:
        rulebased_notifications = watolib.load_configuration_settings().get(
            "enable_rulebased_notifications")
        if rulebased_notifications and config.user.may(
                'general.edit_notifications'):
            html.begin_context_buttons()
            url = "wato.py?mode=user_notifications_p"
            html.context_button(_("Notifications"), url, "notifications")
            html.end_context_buttons()
    else:
        reason = html.request.var('reason')
        if reason == 'expired':
            html.p(
                _('Your password is too old, you need to choose a new password.'
                  ))
        else:
            html.p(
                _('You are required to change your password before proceeding.'
                  ))

    if success:
        html.reload_sidebar()
        if change_pw:
            html.show_message(_("Your password has been changed."))
            raise HTTPRedirect(html.request.var('_origtarget', 'index.py'))
        else:
            html.show_message(_("Successfully updated user profile."))
            # Ensure theme changes are applied without additional user interaction
            html.immediate_browser_redirect(0.5, html.makeuri([]))

    if html.has_user_errors():
        html.show_user_errors()

    user = users.get(config.user.id)
    if user is None:
        html.show_warning(_("Sorry, your user account does not exist."))
        html.footer()
        return

    # Returns true if an attribute is locked and should be read only. Is only
    # checked when modifying an existing user
    locked_attributes = userdb.locked_attributes(user.get('connector'))

    def is_locked(attr):
        return attr in locked_attributes

    html.begin_form("profile", method="POST")
    html.prevent_password_auto_completion()
    html.open_div(class_="wato")
    forms.header(_("Personal Settings"))

    if not change_pw:
        forms.section(_("Name"), simple=True)
        html.write_text(user.get("alias", config.user.id))

    if config.user.may(
            'general.change_password') and not is_locked('password'):
        forms.section(_("Current Password"))
        html.password_input('cur_password', autocomplete="new-password")

        forms.section(_("New Password"))
        html.password_input('password', autocomplete="new-password")

        forms.section(_("New Password Confirmation"))
        html.password_input('password2', autocomplete="new-password")

    if not change_pw and config.user.may('general.edit_profile'):
        select_language(user)

        # Let the user configure how he wants to be notified
        if not rulebased_notifications \
            and config.user.may('general.edit_notifications') \
            and user.get("notifications_enabled"):
            forms.section(_("Notifications"))
            html.help(
                _("Here you can configure how you want to be notified about host and service problems and "
                  "other monitoring events."))
            watolib.get_vs_flexible_notifications().render_input(
                "notification_method", user.get("notification_method"))

        if config.user.may('general.edit_user_attributes'):
            for name, attr in userdb.get_user_attributes():
                if attr.user_editable():
                    vs = attr.valuespec()
                    forms.section(_u(vs.title()))
                    value = user.get(name, vs.default_value())
                    if not attr.permission() or config.user.may(
                            attr.permission()):
                        vs.render_input("ua_" + name, value)
                        html.help(_u(vs.help()))
                    else:
                        html.write(vs.value_to_text(value))

    # Save button
    forms.end()
    html.button("_save", _("Save"))
    html.close_div()
    html.hidden_fields()
    html.end_form()
    html.footer()
Esempio n. 23
0
def do_log_ack(site, host_name, file_name):
    logs_to_ack = []
    if not host_name and not file_name:  # all logs on all hosts
        for this_site, this_host, logs in all_logs():
            for int_filename in logs:
                file_display = form_file_to_ext(int_filename)
                logs_to_ack.append(
                    (this_site, this_host, int_filename, file_display))
        ack_msg = _('all logfiles on all hosts')

    elif host_name and not file_name:  # all logs on one host
        for int_filename in logfiles_of_host(site, host_name):
            file_display = form_file_to_ext(int_filename)
            logs_to_ack.append((site, host_name, int_filename, file_display))
        ack_msg = _('all logfiles of host %s') % host_name

    elif host_name and file_name:  # one log on one host
        int_filename = form_file_to_int(file_name)
        logs_to_ack = [(site, host_name, int_filename,
                        form_file_to_ext(int_filename))]
        ack_msg = _('the log file %s on host %s') % (file_name, host_name)

    else:
        for this_site, this_host, logs in all_logs():
            file_display = form_file_to_ext(file_name)
            if file_name in logs:
                logs_to_ack.append(
                    (this_site, this_host, file_name, file_display))
        ack_msg = _('log file %s on all hosts') % file_name

    title = _("Acknowledge %s") % html.render_text(ack_msg)

    if host_name:
        breadcrumb = make_host_breadcrumb(host_name)
    else:
        breadcrumb = make_simple_page_breadcrumb(MegaMenuMonitoring, title)

    html.header(title, breadcrumb)

    html.begin_context_buttons()
    button_all_logfiles()
    if host_name:
        html.context_button(_("All Logfiles of Host"),
                            html.makeuri([('file', '')]))
    if host_name and file_name:
        html.context_button(_("Back to Logfile"), html.makeuri([]))
    html.end_context_buttons()

    ack = html.request.var('_ack')
    if not html.confirm(
            _("Do you really want to acknowledge %s by <b>deleting</b> all stored messages?"
              ) % ack_msg):
        html.footer()
        return

    if not config.user.may("general.act"):
        html.h1(_('Permission denied'), class_=["error"])
        html.div(_('You are not allowed to acknowledge %s') % ack_msg,
                 class_=["error"])
        html.footer()
        return

    # filter invalid values
    if ack != '1':
        raise MKUserError('_ack', _('Invalid value for ack parameter.'))

    for this_site, this_host, int_filename, display_name in logs_to_ack:
        try:
            acknowledge_logfile(this_site, this_host, int_filename,
                                display_name)
        except Exception as e:
            html.show_error(
                _('The log file <tt>%s</tt> of host <tt>%s</tt> could not be deleted: %s.'
                  ) % (display_name, this_host, e))
            html.footer()
            return

    html.show_message('<b>%s</b><p>%s</p>' %
                      (_('Acknowledged %s') % ack_msg,
                       _('Acknowledged all messages in %s.') % ack_msg))
    html.footer()
Esempio n. 24
0
def show_file(site, host_name, file_name):
    int_filename = form_file_to_int(file_name)

    title = _("Logfiles of Host %s: %s") % (host_name, file_name)
    html.header(title, make_host_breadcrumb(host_name))

    html.begin_context_buttons()
    html.context_button(_("Services"), services_url(site, host_name),
                        'services')
    html.context_button(_("All Logfiles of Host"),
                        html.makeuri([('file', '')]))
    button_all_logfiles()
    html.context_button(_("Analyze patterns"),
                        analyse_url(site, host_name, file_name), 'analyze')

    if html.request.var('_hidecontext', 'no') == 'yes':
        hide_context_label = _('Show Context')
        hide_context_param = 'no'
        hide = True
    else:
        hide_context_label = _('Hide Context')
        hide_context_param = 'yes'
        hide = False

    try:
        log_chunks = parse_file(site, host_name, int_filename, hide)
    except Exception as e:
        if config.debug:
            raise
        html.end_context_buttons()
        html.show_error(_("Unable to show logfile: <b>%s</b>") % e)
        html.footer()
        return

    if log_chunks is None:
        html.end_context_buttons()
        html.show_error(_("The logfile does not exist."))
        html.footer()
        return

    if log_chunks == []:
        html.end_context_buttons()
        html.show_message(
            _("This logfile contains no unacknowledged messages."))
        html.footer()
        return

    ack_button(site, host_name, int_filename)
    html.context_button(hide_context_label,
                        html.makeuri([('_hidecontext', hide_context_param)]))

    html.end_context_buttons()

    html.open_div(id_="logwatch")
    for log in log_chunks:
        html.open_div(class_=["chunk"])
        html.open_table(class_=["section"])
        html.open_tr()
        html.td(form_level(log['level']), class_=form_level(log['level']))
        html.td(form_datetime(log['datetime']), class_="date")
        html.close_tr()
        html.close_table()

        for line in log['lines']:
            html.open_p(class_=line['class'])
            html.icon_button(
                analyse_url(site, host_name, file_name, line['line']),
                _("Analyze this line"), "analyze")
            html.write_text(line['line'].replace(" ", "&nbsp;").replace(
                "\1", "<br>"))
            html.close_p()

        html.close_div()

    html.close_div()
    html.footer()