Refactoring: Moved check parameters from unsorted.py to dedicated modules (CMK-1393)
[check_mk.git] / cmk / gui / htmllib.py
blob989c613fa21f8f062f428b59fad6874d3f15dea1
1 #!/usr/bin/env python
2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
9 # | |
10 # | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
11 # +------------------------------------------------------------------+
13 # This file is part of Check_MK.
14 # The official homepage is at http://mathias-kettner.de/check_mk.
16 # check_mk is free software; you can redistribute it and/or modify it
17 # under the terms of the GNU General Public License as published by
18 # the Free Software Foundation in version 2. check_mk is distributed
19 # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
20 # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
21 # PARTICULAR PURPOSE. See the GNU General Public License for more de-
22 # tails. You should have received a copy of the GNU General Public
23 # License along with GNU Make; see the file COPYING. If not, write
24 # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
25 # Boston, MA 02110-1301 USA.
27 # TODO:
29 # Notes for future rewrite:
31 # - Find all call sites which do something like "int(html.request.var(...))"
32 # and replace it with html.get_integer_input(...)
34 # - Make clear which functions return values and which write out values
35 # render_*, add_*, write_* (e.g. icon() -> outputs directly,
36 # render_icon() -> returns icon
38 # - Order of arguments:
39 # e.g. icon(help, icon) -> change and make help otional?
41 # - Fix names of message() show_error() show_warning()
43 # - change naming of html.attrencode() to html.render()
45 # - General rules:
46 # 1. values of type str that are passed as arguments or
47 # return values or are stored in datastructures must not contain
48 # non-Ascii characters! UTF-8 encoding must just be used in
49 # the last few CPU cycles before outputting. Conversion from
50 # input to str or unicode must happen as early as possible,
51 # directly when reading from file or URL.
53 # - indentify internal helper methods and prefix them with "_"
55 # - Split HTML handling (page generating) code and generic request
56 # handling (vars, cookies, ...) up into separate classes to make
57 # the different tasks clearer. For HTMLGenerator() or similar.
59 import time
60 import os
61 import urllib
62 import ast
63 import random
64 import re
65 import signal
66 import json
67 import abc
68 import pprint
69 from contextlib import contextmanager
71 import six
73 try:
74 # First try python3
75 # suppress missing import error from mypy
76 from html import escape as html_escape # type: ignore
77 except ImportError:
78 # Default to python2
79 from cgi import escape as html_escape
82 # Monkey patch in order to make the HTML class below json-serializable without changing the default json calls.
83 def _default(self, obj):
84 return getattr(obj.__class__, "to_json", _default.default)(obj)
87 # TODO: suppress mypy warnings for this monkey patch right now. See also:
88 # https://github.com/python/mypy/issues/2087
89 # Save unmodified default:
90 _default.default = json.JSONEncoder().default # type: ignore
91 # replacement:
92 json.JSONEncoder.default = _default # type: ignore
94 import cmk.utils.paths
95 from cmk.utils.exceptions import MKGeneralException
96 from cmk.gui.exceptions import MKUserError, RequestTimeout
98 import cmk.gui.utils as utils
99 import cmk.gui.config as config
100 import cmk.gui.log as log
101 from cmk.gui.i18n import _
104 # .--Escaper-------------------------------------------------------------.
105 # | _____ |
106 # | | ____|___ ___ __ _ _ __ ___ _ __ |
107 # | | _| / __|/ __/ _` | '_ \ / _ \ '__| |
108 # | | |___\__ \ (_| (_| | |_) | __/ | |
109 # | |_____|___/\___\__,_| .__/ \___|_| |
110 # | |_| |
111 # +----------------------------------------------------------------------+
112 # | |
113 # '----------------------------------------------------------------------
116 class Escaper(object):
117 def __init__(self):
118 super(Escaper, self).__init__()
119 self._unescaper_text = re.compile(
120 r'&lt;(/?)(h1|h2|b|tt|i|u|br(?: /)?|nobr(?: /)?|pre|a|sup|p|li|ul|ol)&gt;')
121 self._unescaper_href = re.compile(r'&lt;a href=(?:&quot;|\')(.*?)(?:&quot;|\')&gt;')
122 self._unescaper_href_target = re.compile(
123 r'&lt;a href=(?:&quot;|\')(.*?)(?:&quot;|\') target=(?:&quot;|\')(.*?)(?:&quot;|\')&gt;'
126 # Encode HTML attributes. Replace HTML syntax with HTML text.
127 # For example: replace '"' with '&quot;', '<' with '&lt;'.
128 # This code is slow. Works on str and unicode without changing
129 # the type. Also works on things that can be converted with '%s'.
130 def escape_attribute(self, value):
131 attr_type = type(value)
132 if value is None:
133 return ''
134 elif attr_type == int:
135 return str(value)
136 elif isinstance(value, HTML):
137 return "%s" % value # This is HTML code which must not be escaped
138 elif attr_type not in [str, unicode]: # also possible: type Exception!
139 value = "%s" % value # Note: this allows Unicode. value might not have type str now
140 return html_escape(value, quote=True)
142 def unescape_attributes(self, value):
143 # In python3 use html.unescape
144 return value.replace("&amp;", "&")\
145 .replace("&quot;", "\"")\
146 .replace("&lt;", "<")\
147 .replace("&gt;", ">")
149 # render HTML text.
150 # We only strip od some tags and allow some simple tags
151 # such as <h1>, <b> or <i> to be part of the string.
152 # This is useful for messages where we want to keep formatting
153 # options. (Formerly known as 'permissive_attrencode') """
154 # for the escaping functions
155 def escape_text(self, text):
157 if isinstance(text, HTML):
158 return "%s" % text # This is HTML code which must not be escaped
160 text = self.escape_attribute(text)
161 text = self._unescaper_text.sub(r'<\1\2>', text)
162 # Also repair link definitions
163 text = self._unescaper_href_target.sub(r'<a href="\1" target="\2">', text)
164 text = self._unescaper_href.sub(r'<a href="\1">', text)
165 text = re.sub(r'&amp;nbsp;', '&nbsp;', text)
166 return text
170 # .--Encoding------------------------------------------------------------.
171 # | _____ _ _ |
172 # | | ____|_ __ ___ ___ __| (_)_ __ __ _ |
173 # | | _| | '_ \ / __/ _ \ / _` | | '_ \ / _` | |
174 # | | |___| | | | (_| (_) | (_| | | | | | (_| | |
175 # | |_____|_| |_|\___\___/ \__,_|_|_| |_|\__, | |
176 # | |___/ |
177 # +----------------------------------------------------------------------+
178 # | |
179 # '----------------------------------------------------------------------'
182 class Encoder(object):
183 def urlencode_vars(self, vars_):
184 """Convert a mapping object or a sequence of two-element tuples to a “percent-encoded” string
186 This function returns a str object, never unicode!
187 Note: This should be changed once we change everything to
188 unicode internally.
190 assert isinstance(vars_, list)
191 pairs = []
192 for varname, value in sorted(vars_):
193 assert isinstance(varname, str)
195 if isinstance(value, int):
196 value = str(value)
197 elif isinstance(value, unicode):
198 value = value.encode("utf-8")
199 elif value is None:
200 # TODO: This is not ideal and should better be cleaned up somehow. Shouldn't
201 # variables with None values simply be skipped? We currently can not find the
202 # call sites easily. This may be cleaned up once we establish typing. Until then
203 # we need to be compatible with the previous behavior.
204 value = ""
206 #assert type(value) == str, "%s: %s" % (varname, value)
208 pairs.append((varname, value))
210 return urllib.urlencode(pairs)
212 def urlencode(self, value):
213 """Replace special characters in string using the %xx escape.
215 This function returns a str object, never unicode!
216 Note: This should be changed once we change everything to
217 unicode internally.
219 if isinstance(value, unicode):
220 value = value.encode("utf-8")
221 elif value is None:
222 return ""
224 assert isinstance(value, str)
226 return urllib.quote_plus(value)
230 # .--HTML----------------------------------------------------------------.
231 # | _ _ _____ __ __ _ |
232 # | | | | |_ _| \/ | | |
233 # | | |_| | | | | |\/| | | |
234 # | | _ | | | | | | | |___ |
235 # | |_| |_| |_| |_| |_|_____| |
236 # | |
237 # +----------------------------------------------------------------------+
238 # | This is a simple class which wraps a unicode string provided by |
239 # | the caller to make html.attrencode() know that this string should |
240 # | not be escaped. |
241 # | |
242 # | This way we can implement encodings while still allowing HTML code. |
243 # | This is useful when one needs to print out HTML tables in messages |
244 # | or help texts. |
245 # | |
246 # | The HTML class is implemented as an immutable type. |
247 # | Every instance of the class is a unicode string. |
248 # | Only utf-8 compatible encodings are supported. |
249 # '----------------------------------------------------------------------'
252 class HTML(object):
253 def __init__(self, value=u''):
254 super(HTML, self).__init__()
255 self.value = self._ensure_unicode(value)
257 def __unicode__(self):
258 return self.value
260 def _ensure_unicode(self, thing, encoding_index=0):
261 try:
262 return unicode(thing)
263 except UnicodeDecodeError:
264 return thing.decode("utf-8")
266 def __bytebatzen__(self):
267 return self.value.encode("utf-8")
269 def __str__(self):
270 # Against the sense of the __str__() method, we need to return the value
271 # as unicode here. Why? There are many cases where something like
272 # "%s" % HTML(...) is done in the GUI code. This calls the __str__ function
273 # because the origin is a str() object. The call will then return a UTF-8
274 # encoded str() object. This brings a lot of compatbility issues to the code
275 # which can not be solved easily.
276 # To deal with this situation we need the implicit conversion from str to
277 # unicode that happens when we return a unicode value here. In all relevant
278 # cases this does exactly what we need. It would only fail if the origin
279 # string contained characters that can not be decoded with the ascii codec
280 # which is not relevant for us here.
282 # This is problematic:
283 # html.write("%s" % HTML("ä"))
285 # Bottom line: We should relly cleanup internal unicode/str handling.
286 return self.value
288 def __repr__(self):
289 return ("HTML(\"%s\")" % self.value).encode("utf-8")
291 def to_json(self):
292 return self.value
294 def __add__(self, other):
295 return HTML(self.value + self._ensure_unicode(other))
297 def __iadd__(self, other):
298 return self.__add__(other)
300 def __radd__(self, other):
301 return HTML(self._ensure_unicode(other) + self.value)
303 def join(self, iterable):
304 return HTML(self.value.join(map(self._ensure_unicode, iterable)))
306 def __eq__(self, other):
307 return self.value == self._ensure_unicode(other)
309 def __ne__(self, other):
310 return self.value != self._ensure_unicode(other)
312 def __len__(self):
313 return len(self.value)
315 def __getitem__(self, index):
316 return HTML(self.value[index])
318 def __contains__(self, item):
319 return self._ensure_unicode(item) in self.value
321 def count(self, sub, *args):
322 return self.value.count(self._ensure_unicode(sub), *args)
324 def index(self, sub, *args):
325 return self.value.index(self._ensure_unicode(sub), *args)
327 def lstrip(self, *args):
328 args = tuple(map(self._ensure_unicode, args[:1])) + args[1:]
329 return HTML(self.value.lstrip(*args))
331 def rstrip(self, *args):
332 args = tuple(map(self._ensure_unicode, args[:1])) + args[1:]
333 return HTML(self.value.rstrip(*args))
335 def strip(self, *args):
336 args = tuple(map(self._ensure_unicode, args[:1])) + args[1:]
337 return HTML(self.value.strip(*args))
339 def lower(self):
340 return HTML(self.value.lower())
342 def upper(self):
343 return HTML(self.value.upper())
345 def startswith(self, prefix, *args):
346 return self.value.startswith(self._ensure_unicode(prefix), *args)
350 # .--OutputFunnel--------------------------------------------------------.
351 # | ___ _ _ _____ _ |
352 # | / _ \ _ _| |_ _ __ _ _| |_| ___| _ _ __ _ __ ___| | |
353 # | | | | | | | | __| '_ \| | | | __| |_ | | | | '_ \| '_ \ / _ \ | |
354 # | | |_| | |_| | |_| |_) | |_| | |_| _|| |_| | | | | | | | __/ | |
355 # | \___/ \__,_|\__| .__/ \__,_|\__|_| \__,_|_| |_|_| |_|\___|_| |
356 # | |_| |
357 # +----------------------------------------------------------------------+
358 # | Provides the write functionality. The method _lowlevel_write needs |
359 # | to be overwritten in the specific subclass! |
360 # | |
361 # | Usage of plugged context: |
362 # | with html.plugged(): |
363 # | html.write("something") |
364 # | html_code = html.drain() |
365 # | print html_code |
366 # '----------------------------------------------------------------------'
369 class OutputFunnel(object):
370 __metaclass__ = abc.ABCMeta
372 def __init__(self):
373 super(OutputFunnel, self).__init__()
374 self.plug_text = []
376 # Accepts str and unicode objects only!
377 # The plugged functionality can be used for debugging.
378 def write(self, text):
379 if not text:
380 return
382 if isinstance(text, HTML):
383 text = "%s" % text
385 if not isinstance(text, six.string_types): # also possible: type Exception!
386 raise MKGeneralException(
387 _('Type Error: html.write accepts str and unicode input objects only!'))
389 if self._is_plugged():
390 self.plug_text[-1].append(text)
391 else:
392 # encode when really writing out the data. Not when writing plugged,
393 # because the plugged code will be handled somehow by our code. We
394 # only encode when leaving the pythonic world.
395 if isinstance(text, unicode):
396 text = text.encode("utf-8")
397 self._lowlevel_write(text)
399 @abc.abstractmethod
400 def _lowlevel_write(self, text):
401 raise NotImplementedError()
403 @contextmanager
404 def plugged(self):
405 self.plug_text.append([])
406 try:
407 yield
408 finally:
409 text = self.drain()
410 self.plug_text.pop()
411 self.write(text)
413 def _is_plugged(self):
414 return bool(self.plug_text)
416 # Get the sink content in order to do something with it.
417 def drain(self):
418 if not self._is_plugged(): # TODO: Raise exception or even remove "if"?
419 return ''
421 text = "".join(self.plug_text.pop())
422 self.plug_text.append([])
423 return text
427 # .--HTML Generator------------------------------------------------------.
428 # | _ _ _____ __ __ _ |
429 # | | | | |_ _| \/ | | |
430 # | | |_| | | | | |\/| | | |
431 # | | _ | | | | | | | |___ |
432 # | |_| |_| |_| |_| |_|_____| |
433 # | |
434 # | ____ _ |
435 # | / ___| ___ _ __ ___ _ __ __ _| |_ ___ _ __ |
436 # | | | _ / _ \ '_ \ / _ \ '__/ _` | __/ _ \| '__| |
437 # | | |_| | __/ | | | __/ | | (_| | || (_) | | |
438 # | \____|\___|_| |_|\___|_| \__,_|\__\___/|_| |
439 # | |
440 # +----------------------------------------------------------------------+
441 # | Generator which provides top level HTML writing functionality. |
442 # '----------------------------------------------------------------------'
445 class HTMLGenerator(OutputFunnel):
446 """ Usage Notes:
448 - Tags can be opened using the open_[tag]() call where [tag] is one of the possible tag names.
449 All attributes can be passed as function arguments, such as open_div(class_="example").
450 However, python specific key words need to be escaped using a trailing underscore.
451 One can also provide a dictionary as attributes: open_div(**{"class": "example"}).
453 - All tags can be closed again using the close_[tag]() syntax.
455 - For tags which shall only contain plain text (i.e. no tags other than highlighting tags)
456 you can a the direct call using the tag name only as function name,
457 self.div("Text content", **attrs). Tags featuring this functionality are listed in
458 the "featured shortcuts" list.
460 - Some tags require mandatory arguments. Those are defined explicitly below.
461 For example an a tag needs the href attribute everytime.
463 - If you want to provide plain HTML to a tag, please use the tag_content function or
464 facillitate the HTML class.
466 HOWTO HTML Attributes:
468 - Python specific attributes have to be escaped using a trailing underscore
470 - All attributes can be python objects. However, some attributes can also be lists of attrs:
472 'class' attributes will be concatenated using one whitespace
473 'style' attributes will be concatenated using the semicolon and one whitespace
474 Behaviorial attributes such as 'onclick', 'onmouseover' will bec concatenated using
475 a semicolon and one whitespace.
477 - All attributes will be escaped, i.e. the characters '&', '<', '>', '"' will be replaced by
478 non HtML relevant signs '&amp;', '&lt;', '&gt;' and '&quot;'. """
480 # TODO: Replace u, i, b with underline, italic, bold, usw.
482 # these tags can be called by their tag names, e.g. 'self.title(content)'
483 _shortcut_tags = set(["title", "h1", "h2", "h3", "h4", "th", "tr", "td", "center", "pre", "style", "iframe",\
484 "div", "p", "span", "canvas", "strong", "sub", "tt", "u", "i", "b", "x", "option"])
486 # these tags can be called by open_name(), close_name() and render_name(), e.g. 'self.open_html()'
487 _tag_names = set(['html', 'head', 'body', 'header', 'footer', 'a', 'b', 'sup',\
488 'script', 'form', 'button', 'p', 'select', 'fieldset',\
489 'table', 'tbody', 'row', 'ul', 'li', 'br', 'nobr', 'input', 'span'])
491 # Of course all shortcut tags can be used as well.
492 _tag_names.update(_shortcut_tags)
494 def __init__(self):
495 super(HTMLGenerator, self).__init__()
496 self.escaper = Escaper()
499 # Rendering
502 def _render_attributes(self, **attrs):
503 # make class attribute foolproof
504 css = []
505 for k in ["class_", "css", "cssclass", "class"]:
506 if k in attrs:
507 if isinstance(attrs[k], list):
508 css.extend(attrs.pop(k))
509 elif attrs[k] is not None:
510 css.append(attrs.pop(k))
511 if css:
512 attrs["class"] = css
514 # options such as 'selected' and 'checked' dont have a value in html tags
515 options = []
517 # render all attributes
518 for k, v in attrs.iteritems():
520 if v is None:
521 continue
523 k = self.escaper.escape_attribute(k.rstrip('_'))
525 if v == '':
526 options.append(k)
527 continue
529 if not isinstance(v, list):
530 v = self.escaper.escape_attribute(v)
531 else:
532 if k == "class":
533 sep = ' '
534 elif k == "style" or k.startswith('on'):
535 sep = '; '
536 else:
537 sep = '_'
539 v = sep.join([a for a in (self.escaper.escape_attribute(vi) for vi in v) if a])
541 if sep.startswith(';'):
542 v = re.sub(';+', ';', v)
544 yield ' %s=\"%s\"' % (k, v)
546 for k in options:
547 yield " %s=\'\'" % k
549 # applies attribute encoding to prevent code injections.
550 def _render_opening_tag(self, tag_name, close_tag=False, **attrs):
551 """ You have to replace attributes which are also python elements such as
552 'class', 'id', 'for' or 'type' using a trailing underscore (e.g. 'class_' or 'id_'). """
553 return HTML("<%s%s%s>" % (tag_name,\
554 '' if not attrs else ''.join(self._render_attributes(**attrs)),\
555 '' if not close_tag else ' /'))
557 def _render_closing_tag(self, tag_name):
558 return HTML("</%s>" % (tag_name))
560 def _render_content_tag(self, tag_name, tag_content, **attrs):
561 tag = self._render_opening_tag(tag_name, **attrs)
563 if tag_content in ['', None]:
564 pass
566 else:
567 tag = tag.rstrip("\n")
568 if isinstance(tag_content, HTML):
569 tag += tag_content.lstrip(' ').rstrip('\n')
570 else:
571 tag += self.escaper.escape_text(tag_content)
573 tag += "</%s>" % (tag_name)
575 return HTML(tag)
577 # This is used to create all the render_tag() and close_tag() functions
578 def __getattr__(self, name):
579 """ All closing tags can be called like this:
580 self.close_html(), self.close_tr(), etc. """
582 parts = name.split('_')
584 # generating the shortcut tag calls
585 if len(parts) == 1 and name in self._shortcut_tags:
586 return lambda content, **attrs: self.write_html(self._render_content_tag(name, content, **attrs))
588 # generating the open, close and render calls
589 elif len(parts) == 2:
590 what, tag_name = parts[0], parts[1]
592 if what == "open" and tag_name in self._tag_names:
593 return lambda **attrs: self.write_html(self._render_opening_tag(tag_name, **attrs))
595 elif what == "close" and tag_name in self._tag_names:
596 return lambda: self.write_html(self._render_closing_tag(tag_name))
598 elif what == "render" and tag_name in self._tag_names:
599 return lambda content, **attrs: HTML(self._render_content_tag(tag_name, content, **attrs))
601 else:
602 # FIXME: This returns None, which is not a very informative error message
603 return object.__getattribute__(self, name)
606 # HTML element methods
607 # If an argument is mandatory, it is used as default and it will overwrite an
608 # implicit argument (e.g. id_ will overwrite attrs["id"]).
612 # basic elements
615 def render_text(self, text):
616 return HTML(self.escaper.escape_text(text))
618 def write_text(self, text):
619 """ Write text. Highlighting tags such as h2|b|tt|i|br|pre|a|sup|p|li|ul|ol are not escaped. """
620 self.write(self.render_text(text))
622 def write_html(self, content):
623 """ Write HTML code directly, without escaping. """
624 self.write(content)
626 def comment(self, comment_text):
627 self.write("<!--%s-->" % self.encode_attribute(comment_text))
629 def meta(self, httpequiv=None, **attrs):
630 if httpequiv:
631 attrs['http-equiv'] = httpequiv
632 self.write_html(self._render_opening_tag('meta', close_tag=True, **attrs))
634 def base(self, target):
635 self.write_html(self._render_opening_tag('base', close_tag=True, target=target))
637 def open_a(self, href, **attrs):
638 attrs['href'] = href
639 self.write_html(self._render_opening_tag('a', **attrs))
641 def render_a(self, content, href, **attrs):
642 attrs['href'] = href
643 return self._render_content_tag('a', content, **attrs)
645 def a(self, content, href, **attrs):
646 self.write_html(self.render_a(content, href, **attrs))
648 def stylesheet(self, href):
649 self.write_html(
650 self._render_opening_tag(
651 'link', rel="stylesheet", type_="text/css", href=href, close_tag=True))
654 # Scriptingi
657 def render_javascript(self, code):
658 return HTML("<script type=\"text/javascript\">\n%s\n</script>\n" % code)
660 def javascript(self, code):
661 self.write_html(self.render_javascript(code))
663 def javascript_file(self, src):
664 """ <script type="text/javascript" src="%(name)"/>\n """
665 self.write_html(self._render_content_tag('script', '', type_="text/javascript", src=src))
667 def render_img(self, src, **attrs):
668 attrs['src'] = src
669 return self._render_opening_tag('img', close_tag=True, **attrs)
671 def img(self, src, **attrs):
672 self.write_html(self.render_img(src, **attrs))
674 def open_button(self, type_, **attrs):
675 attrs['type'] = type_
676 self.write_html(self._render_opening_tag('button', close_tag=True, **attrs))
678 def play_sound(self, url):
679 self.write_html(self._render_opening_tag('audio autoplay', src_=url))
682 # form elements
685 def render_label(self, content, for_, **attrs):
686 attrs['for'] = for_
687 return self._render_content_tag('label', content, **attrs)
689 def label(self, content, for_, **attrs):
690 self.write_html(self.render_label(content, for_, **attrs))
692 def render_input(self, name, type_, **attrs):
693 attrs['type_'] = type_
694 attrs['name'] = name
695 return self._render_opening_tag('input', close_tag=True, **attrs)
697 def input(self, name, type_, **attrs):
698 self.write_html(self.render_input(name, type_, **attrs))
701 # table and list elements
704 def td(self, content, **attrs):
705 """ Only for text content. You can't put HTML structure here. """
706 self.write_html(self._render_content_tag('td', content, **attrs))
708 def li(self, content, **attrs):
709 """ Only for text content. You can't put HTML structure here. """
710 self.write_html(self._render_content_tag('li', content, **attrs))
713 # structural text elements
716 def render_heading(self, content):
717 """ <h2>%(content)</h2> """
718 return self._render_content_tag('h2', content)
720 def heading(self, content):
721 self.write_html(self.render_heading(content))
723 def render_br(self):
724 return HTML("<br/>")
726 def br(self):
727 self.write_html(self.render_br())
729 def render_hr(self, **attrs):
730 return self._render_opening_tag('hr', close_tag=True, **attrs)
732 def hr(self, **attrs):
733 self.write_html(self.render_hr(**attrs))
735 def rule(self):
736 return self.hr()
738 def render_nbsp(self):
739 return HTML("&nbsp;")
741 def nbsp(self):
742 self.write_html(self.render_nbsp())
746 # .--TimeoutMgr.---------------------------------------------------------.
747 # | _____ _ _ __ __ |
748 # | |_ _(_)_ __ ___ ___ ___ _ _| |_| \/ | __ _ _ __ |
749 # | | | | | '_ ` _ \ / _ \/ _ \| | | | __| |\/| |/ _` | '__| |
750 # | | | | | | | | | | __/ (_) | |_| | |_| | | | (_| | | _ |
751 # | |_| |_|_| |_| |_|\___|\___/ \__,_|\__|_| |_|\__, |_|(_) |
752 # | |___/ |
753 # '----------------------------------------------------------------------'
756 class TimeoutManager(object):
757 """Request timeout handling
759 The system apache process will end the communication with the client after
760 the timeout configured for the proxy connection from system apache to site
761 apache. This is done in /omd/sites/[site]/etc/apache/proxy-port.conf file
762 in the "timeout=x" parameter of the ProxyPass statement.
764 The regular request timeout configured here should always be lower to make
765 it possible to abort the page processing and send a helpful answer to the
766 client.
768 It is possible to disable the applications request timeout (temoporarily)
769 or totally for specific calls, but the timeout to the client will always
770 be applied by the system webserver. So the client will always get a error
771 page while the site apache continues processing the request (until the
772 first try to write anything to the client) which will result in an
773 exception.
776 def enable_timeout(self, duration):
777 def handle_request_timeout(signum, frame):
778 raise RequestTimeout(
779 _("Your request timed out after %d seconds. This issue may be "
780 "related to a local configuration problem or a request which works "
781 "with a too large number of objects. But if you think this "
782 "issue is a bug, please send a crash report.") % duration)
784 signal.signal(signal.SIGALRM, handle_request_timeout)
785 signal.alarm(duration)
787 def disable_timeout(self):
788 signal.alarm(0)
792 # .--Transactions--------------------------------------------------------.
793 # | _____ _ _ |
794 # | |_ _| __ __ _ _ __ ___ __ _ ___| |_(_) ___ _ __ ___ |
795 # | | || '__/ _` | '_ \/ __|/ _` |/ __| __| |/ _ \| '_ \/ __| |
796 # | | || | | (_| | | | \__ \ (_| | (__| |_| | (_) | | | \__ \ |
797 # | |_||_| \__,_|_| |_|___/\__,_|\___|\__|_|\___/|_| |_|___/ |
798 # | |
799 # '----------------------------------------------------------------------'
802 class TransactionManager(object):
803 """Manages the handling of transaction IDs used by the GUI to prevent against
804 performing the same action multiple times."""
806 def __init__(self, request):
807 super(TransactionManager, self).__init__()
808 self._request = request
810 self._new_transids = []
811 self._ignore_transids = False
812 self._current_transid = None
814 def ignore(self):
815 """Makes the GUI skip all transaction validation steps"""
816 self._ignore_transids = True
818 def get(self):
819 """Returns a transaction ID that can be used during a subsequent action"""
820 if not self._current_transid:
821 self._current_transid = self.fresh_transid()
822 return self._current_transid
824 def fresh_transid(self):
825 """Compute a (hopefully) unique transaction id.
827 This is generated during rendering of a form or an action link, stored
828 in a user specific file for later validation, sent to the users browser
829 via HTML code, then submitted by the user together with the action
830 (link / form) and then validated if it is a known transid. When it is a
831 known transid, it will be used and invalidated. If the id is not known,
832 the action will not be processed."""
833 transid = "%d/%d" % (int(time.time()), random.getrandbits(32))
834 self._new_transids.append(transid)
835 return transid
837 def store_new(self):
838 """All generated transids are saved per user.
840 They are stored in the transids.mk. Per user only up to 20 transids of
841 the already existing ones are kept. The transids generated on the
842 current page are all kept. IDs older than one day are deleted."""
843 if not self._new_transids:
844 return
846 valid_ids = self._load_transids(lock=True)
847 cleared_ids = []
848 now = time.time()
849 for valid_id in valid_ids:
850 timestamp = valid_id.split("/")[0]
851 if now - int(timestamp) < 86400: # one day
852 cleared_ids.append(valid_id)
853 self._save_transids((cleared_ids[-20:] + self._new_transids))
855 def transaction_valid(self):
856 """Checks if the current transaction is valid
858 i.e. in case of browser reload a browser reload, the form submit should
859 not be handled a second time.. The HTML variable _transid must be
860 present.
862 In case of automation users (authed by _secret in URL): If it is empty
863 or -1, then it's always valid (this is used for webservice calls).
864 This was also possible for normal users, but has been removed to preven
865 security related issues."""
866 if not self._request.has_var("_transid"):
867 return False
869 transid = self._request.var("_transid")
870 if self._ignore_transids and (not transid or transid == '-1'):
871 return True # automation
873 if '/' not in transid:
874 return False
876 # Normal user/password auth user handling
877 timestamp = transid.split("/", 1)[0]
879 # If age is too old (one week), it is always
880 # invalid:
881 now = time.time()
882 if now - int(timestamp) >= 604800: # 7 * 24 hours
883 return False
885 # Now check, if this transid is a valid one
886 return transid in self._load_transids()
888 def is_transaction(self):
889 """Checks, if the current page is a transation, i.e. something that is secured by
890 a transid (such as a submitted form)"""
891 return self._request.has_var("_transid")
893 def check_transaction(self):
894 """called by page functions in order to check, if this was a reload or the original form submission.
896 Increases the transid of the user, if the latter was the case.
898 There are three return codes:
900 True: -> positive confirmation by the user
901 False: -> not yet confirmed, question is being shown
902 None: -> a browser reload or a negative confirmation
904 if self.transaction_valid():
905 transid = self._request.var("_transid")
906 if transid and transid != "-1":
907 self._invalidate(transid)
908 return True
909 else:
910 return False
912 def _invalidate(self, used_id):
913 """Remove the used transid from the list of valid ones"""
914 valid_ids = self._load_transids(lock=True)
915 try:
916 valid_ids.remove(used_id)
917 except ValueError:
918 return
919 self._save_transids(valid_ids)
921 def _load_transids(self, lock=False):
922 return config.user.load_file("transids", [], lock)
924 def _save_transids(self, used_ids):
925 if config.user.id:
926 config.user.save_file("transids", used_ids)
930 # .--html----------------------------------------------------------------.
931 # | _ _ _ |
932 # | | |__ | |_ _ __ ___ | | |
933 # | | '_ \| __| '_ ` _ \| | |
934 # | | | | | |_| | | | | | | |
935 # | |_| |_|\__|_| |_| |_|_| |
936 # | |
937 # +----------------------------------------------------------------------+
938 # | Caution! The class needs to be derived from Outputfunnel first! |
939 # '----------------------------------------------------------------------'
942 class html(HTMLGenerator):
943 def __init__(self, request, response):
944 super(html, self).__init__()
946 self._logger = log.logger.getChild("html")
948 # rendering state
949 self._header_sent = False
950 self._context_buttons_open = False
952 # style options
953 self._body_classes = ['main']
954 self._default_stylesheets = ["main_min", "check_mk", "graphs"]
955 self._default_javascripts = ["main"]
957 # behaviour options
958 self.render_headfoot = True
959 self.enable_debug = False
960 self.screenshotmode = False
961 self.have_help = False
962 self.help_visible = False
964 # browser options
965 self.output_format = "html"
966 self.browser_reload = 0
967 self.browser_redirect = ''
968 self.link_target = None
969 self.myfile = None
971 # Browser options
972 self._user_id = None
973 self.user_errors = {}
974 self.focus_object = None
975 self.events = set([]) # currently used only for sounds
976 self.status_icons = {}
977 self.final_javascript_code = ""
978 self.caches = {}
979 self.treestates = None
980 self.page_context = {}
982 # Settings
983 self.mobile = False
984 self._theme = None
986 # Forms
987 self.form_name = None
988 self.form_vars = []
990 # Time measurement
991 self.times = {}
992 self.start_time = time.time()
993 self.last_measurement = self.start_time
995 # Register helpers
996 self.encoder = Encoder()
997 self.timeout_manager = TimeoutManager()
998 self.transaction_manager = TransactionManager(request)
999 self.request = request
1000 self.response = response
1002 self.enable_request_timeout()
1004 self.response.headers["Content-type"] = "text/html; charset=UTF-8"
1006 self.init_mobile()
1008 self.myfile = self._requested_file_name()
1010 # Disable caching for all our pages as they are mostly dynamically generated,
1011 # user related and are required to be up-to-date on every refresh
1012 self.response.headers["Cache-Control"] = "no-cache"
1014 try:
1015 self.set_output_format(self.get_ascii_input("output_format", "html").lower())
1016 except (MKUserError, MKGeneralException):
1017 pass # Silently ignore unsupported formats
1019 def _lowlevel_write(self, text):
1020 self.response.stream.write(text)
1022 def init_modes(self):
1023 """Initializes the operation mode of the html() object. This is called
1024 after the Check_MK GUI configuration has been loaded, so it is safe
1025 to rely on the config."""
1026 self._verify_not_using_threaded_mpm()
1028 self._init_screenshot_mode()
1029 self._init_debug_mode()
1030 self.init_theme()
1032 def init_theme(self):
1033 self.set_theme(config.ui_theme)
1035 def set_theme(self, theme_id):
1036 # type: (str) -> None
1037 self._theme = theme_id or config.ui_theme
1039 def get_theme(self):
1040 # type: () -> str
1041 return self._theme
1043 def _verify_not_using_threaded_mpm(self):
1044 if self.request.is_multithreaded:
1045 raise MKGeneralException(
1046 _("You are trying to Check_MK together with a threaded Apache multiprocessing module (MPM). "
1047 "Check_MK is only working with the prefork module. Please change the MPM module to make "
1048 "Check_MK work."))
1050 def _init_debug_mode(self):
1051 # Debug flag may be set via URL to override the configuration
1052 if self.request.var("debug"):
1053 config.debug = True
1054 self.enable_debug = config.debug
1056 # Enabling the screenshot mode omits the fancy background and
1057 # makes it white instead.
1058 def _init_screenshot_mode(self):
1059 if self.request.var("screenshotmode", config.screenshotmode):
1060 self.screenshotmode = True
1062 def _requested_file_name(self):
1063 parts = self.request.requested_file.rstrip("/").split("/")
1065 if len(parts) == 3 and parts[-1] == "check_mk":
1066 # Load index page when accessing /[site]/check_mk
1067 myfile = "index"
1069 elif parts[-1].endswith(".py"):
1070 # Regular pages end with .py - Stript it away to get the page name
1071 myfile = parts[-1][:-3]
1072 if myfile == "":
1073 myfile = "index"
1075 else:
1076 myfile = "index"
1078 # Redirect to mobile GUI if we are a mobile device and the index is requested
1079 if myfile == "index" and self.mobile:
1080 myfile = "mobile"
1082 return myfile
1084 def init_mobile(self):
1085 if self.request.has_var("mobile"):
1086 # TODO: Make private
1087 self.mobile = bool(self.request.var("mobile"))
1088 # Persist the explicitly set state in a cookie to have it maintained through further requests
1089 self.response.set_http_cookie("mobile", str(int(self.mobile)))
1091 elif self.request.has_cookie("mobile"):
1092 self.mobile = self.request.cookie("mobile", "0") == "1"
1094 else:
1095 self.mobile = self._is_mobile_client(self.request.user_agent)
1097 def _is_mobile_client(self, user_agent):
1098 # These regexes are taken from the public domain code of Matt Sullivan
1099 # http://sullerton.com/2011/03/django-mobile-browser-detection-middleware/
1100 reg_b = re.compile(
1101 r"android.+mobile|avantgo|bada\\/|blackberry|bb10|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|symbian|treo|up\\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino",
1102 re.I | re.M)
1103 reg_v = re.compile(
1104 r"1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\\-(n|u)|c55\\/|capi|ccwa|cdm\\-|cell|chtm|cldc|cmd\\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\\-s|devi|dica|dmob|do(c|p)o|ds(12|\\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\\-|_)|g1 u|g560|gene|gf\\-5|g\\-mo|go(\\.w|od)|gr(ad|un)|haie|hcit|hd\\-(m|p|t)|hei\\-|hi(pt|ta)|hp( i|ip)|hs\\-c|ht(c(\\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\\-(20|go|ma)|i230|iac( |\\-|\\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\\/)|klon|kpt |kwc\\-|kyo(c|k)|le(no|xi)|lg( g|\\/(k|l|u)|50|54|e\\-|e\\/|\\-[a-w])|libw|lynx|m1\\-w|m3ga|m50\\/|ma(te|ui|xo)|mc(01|21|ca)|m\\-cr|me(di|rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\\-2|po(ck|rt|se)|prox|psio|pt\\-g|qa\\-a|qc(07|12|21|32|60|\\-[2-7]|i\\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\\-|oo|p\\-)|sdk\\/|se(c(\\-|0|1)|47|mc|nd|ri)|sgh\\-|shar|sie(\\-|m)|sk\\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\\-|v\\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\\-|tdg\\-|tel(i|m)|tim\\-|t\\-mo|to(pl|sh)|ts(70|m\\-|m3|m5)|tx\\-9|up(\\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|xda(\\-|2|g)|yas\\-|your|zeto|zte\\-",
1105 re.I | re.M)
1107 return reg_b.search(user_agent) or reg_v.search(user_agent[0:4])
1110 # HTTP variable processing
1113 @contextmanager
1114 def stashed_vars(self):
1115 saved_vars = dict(self.request.itervars())
1116 try:
1117 yield
1118 finally:
1119 self.request.del_vars()
1120 for varname, value in saved_vars.iteritems():
1121 self.request.set_var(varname, value)
1123 def get_ascii_input(self, varname, deflt=None):
1124 """Helper to retrieve a byte string and ensure it only contains ASCII characters
1125 In case a non ASCII character is found an MKUserError() is raised."""
1126 try:
1127 value = self.request.var(varname, deflt)
1128 if value is not None:
1129 value.decode("ascii")
1130 return value
1131 except UnicodeDecodeError:
1132 raise MKUserError(varname, _("The given text must only contain ASCII characters."))
1134 def get_unicode_input(self, varname, deflt=None):
1135 try:
1136 val = self.request.var(varname, deflt)
1137 return val.decode("utf-8") if isinstance(val, str) else val
1138 except UnicodeDecodeError:
1139 raise MKUserError(
1140 varname,
1141 _("The given text is wrong encoded. "
1142 "You need to provide a UTF-8 encoded text."))
1144 def get_integer_input(self, varname, deflt=None):
1145 if deflt is not None and not self.request.has_var(varname):
1146 return deflt
1148 try:
1149 return int(self.request.var(varname))
1150 except TypeError:
1151 raise MKUserError(varname, _("The parameter \"%s\" is missing.") % varname)
1152 except ValueError:
1153 raise MKUserError(varname, _("The parameter \"%s\" is not an integer.") % varname)
1155 # TODO: Invalid default URL is not validated. Should we do it?
1156 # TODO: This is only protecting against some not allowed URLs but does not
1157 # really verify that this is some kind of URL.
1158 def get_url_input(self, varname, deflt=None):
1159 """Helper function to retrieve a URL from HTTP parameters
1161 This is mostly used to the "back url" which can then be used to create
1162 a link to the previous page. For this kind of functionality it is
1163 necessary to restrict the URLs to prevent different attacks on users.
1165 In case the parameter is not given or is not valid the deflt URL will
1166 be used. In case no deflt URL is given a MKUserError() is raised.
1168 if not self.request.has_var(varname):
1169 if deflt is not None:
1170 return deflt
1171 raise MKUserError(varname, _("The parameter \"%s\" is missing.") % varname)
1173 url = self.request.var(varname)
1174 if not utils.is_allowed_url(url):
1175 if deflt:
1176 return deflt
1177 raise MKUserError(varname, _("The parameter \"%s\" is not a valid URL.") % varname)
1179 return url
1181 # Returns a dictionary containing all parameters the user handed over to this request.
1182 # The concept is that the user can either provide the data in a single "request" variable,
1183 # which contains the request data encoded as JSON, or provide multiple GET/POST vars which
1184 # are then used as top level entries in the request object.
1185 def get_request(self, exclude_vars=None):
1186 if exclude_vars is None:
1187 exclude_vars = []
1189 if self.request.var("request_format") == "python":
1190 try:
1191 python_request = self.request.var("request", "{}")
1192 request = ast.literal_eval(python_request)
1193 except (SyntaxError, ValueError) as e:
1194 raise MKUserError(
1195 "request",
1196 _("Failed to parse Python request: '%s': %s") % (python_request, e))
1197 else:
1198 try:
1199 json_request = self.request.var("request", "{}")
1200 request = json.loads(json_request)
1201 request["request_format"] = "json"
1202 except ValueError as e: # Python3: json.JSONDecodeError
1203 raise MKUserError("request",
1204 _("Failed to parse JSON request: '%s': %s") % (json_request, e))
1206 for key, val in self.request.itervars():
1207 if key not in ["request", "output_format"] + exclude_vars:
1208 request[key] = val.decode("utf-8")
1210 return request
1213 # Transaction IDs
1216 # TODO: Cleanup all call sites to self.transaction_manager.*
1217 def transaction_valid(self):
1218 return self.transaction_manager.transaction_valid()
1220 # TODO: Cleanup all call sites to self.transaction_manager.*
1221 def is_transaction(self):
1222 return self.transaction_manager.is_transaction()
1224 # TODO: Cleanup all call sites to self.transaction_manager.*
1225 def check_transaction(self):
1226 return self.transaction_manager.check_transaction()
1229 # Encoding
1232 # TODO: Cleanup all call sites to self.encoder.*
1233 def urlencode_vars(self, vars_):
1234 return self.encoder.urlencode_vars(vars_)
1236 # TODO: Cleanup all call sites to self.encoder.*
1237 def urlencode(self, value):
1238 return self.encoder.urlencode(value)
1241 # escaping - deprecated functions
1243 # Encode HTML attributes: e.g. replace '"' with '&quot;', '<' and '>' with '&lt;' and '&gt;'
1244 # TODO: Cleanup all call sites to self.escaper.*
1245 def attrencode(self, value):
1246 return self.escaper.escape_attribute(value)
1248 # Only strip off some tags. We allow some simple tags like <b> or <tt>.
1249 # TODO: Cleanup all call sites to self.escaper.*
1250 def permissive_attrencode(self, obj):
1251 return self.escaper.escape_text(obj)
1254 # Stripping
1257 # remove all HTML-tags
1258 def strip_tags(self, ht):
1260 if isinstance(ht, HTML):
1261 ht = "%s" % ht
1263 if not isinstance(ht, six.string_types):
1264 return ht
1266 while True:
1267 x = ht.find('<')
1268 if x == -1:
1269 break
1270 y = ht.find('>', x)
1271 if y == -1:
1272 break
1273 ht = ht[0:x] + ht[y + 1:]
1274 return ht.replace("&nbsp;", " ")
1276 def strip_scripts(self, ht):
1277 while True:
1278 x = ht.find('<script')
1279 if x == -1:
1280 break
1281 y = ht.find('</script>')
1282 if y == -1:
1283 break
1284 ht = ht[0:x] + ht[y + 9:]
1285 return ht
1288 # Timeout handling
1291 def enable_request_timeout(self):
1292 self.timeout_manager.enable_timeout(self.request.request_timeout)
1294 def disable_request_timeout(self):
1295 self.timeout_manager.disable_timeout()
1298 # Content Type
1301 def set_output_format(self, f):
1302 if f == "json":
1303 content_type = "application/json; charset=UTF-8"
1305 elif f == "jsonp":
1306 content_type = "application/javascript; charset=UTF-8"
1308 elif f in ("csv", "csv_export"): # Cleanup: drop one of these
1309 content_type = "text/csv; charset=UTF-8"
1311 elif f == "python":
1312 content_type = "text/plain; charset=UTF-8"
1314 elif f == "text":
1315 content_type = "text/plain; charset=UTF-8"
1317 elif f == "html":
1318 content_type = "text/html; charset=UTF-8"
1320 elif f == "xml":
1321 content_type = "text/xml; charset=UTF-8"
1323 elif f == "pdf":
1324 content_type = "application/pdf"
1326 else:
1327 raise MKGeneralException(_("Unsupported context type '%s'") % f)
1329 self.output_format = f
1330 self.response.headers["Content-type"] = content_type
1332 def is_api_call(self):
1333 return self.output_format != "html"
1336 # Other things
1339 def measure_time(self, name):
1340 self.times.setdefault(name, 0.0)
1341 now = time.time()
1342 elapsed = now - self.last_measurement
1343 self.times[name] += elapsed
1344 self.last_measurement = now
1346 def set_user_id(self, user_id):
1347 self._user_id = user_id
1348 # TODO: Shouldn't this be moved to some other place?
1349 self.help_visible = config.user.load_file("help", False) # cache for later usage
1351 def is_mobile(self):
1352 return self.mobile
1354 def set_page_context(self, c):
1355 self.page_context = c
1357 def set_link_target(self, framename):
1358 self.link_target = framename
1360 def set_focus(self, varname):
1361 self.focus_object = (self.form_name, varname)
1363 def set_focus_by_id(self, dom_id):
1364 self.focus_object = dom_id
1366 def set_render_headfoot(self, render):
1367 self.render_headfoot = render
1369 def set_browser_reload(self, secs):
1370 self.browser_reload = secs
1372 def set_browser_redirect(self, secs, url):
1373 self.browser_reload = secs
1374 self.browser_redirect = url
1376 def clear_default_stylesheet(self):
1377 del self._default_javascripts[:]
1379 def add_default_stylesheet(self, name):
1380 if name not in self._default_stylesheets:
1381 self._default_stylesheets.append(name)
1383 def clear_default_javascript(self):
1384 del self._default_javascripts[:]
1386 def add_default_javascript(self, name):
1387 if name not in self._default_javascripts:
1388 self._default_javascripts.append(name)
1390 def immediate_browser_redirect(self, secs, url):
1391 self.javascript("cmk.utils.set_reload(%s, '%s');" % (secs, url))
1393 def add_body_css_class(self, cls):
1394 self._body_classes.append(cls)
1396 def add_status_icon(self, img, tooltip, url=None):
1397 if url:
1398 self.status_icons[img] = tooltip, url
1399 else:
1400 self.status_icons[img] = tooltip
1402 def final_javascript(self, code):
1403 self.final_javascript_code += code + "\n"
1405 def reload_sidebar(self):
1406 if not self.request.has_var("_ajaxid"):
1407 self.write_html(self.render_reload_sidebar())
1409 def render_reload_sidebar(self):
1410 return self.render_javascript("cmk.utils.reload_sidebar()")
1413 # Tree states
1416 def get_tree_states(self, tree):
1417 self.load_tree_states()
1418 return self.treestates.get(tree, {})
1420 def set_tree_state(self, tree, key, val):
1421 self.load_tree_states()
1423 if tree not in self.treestates:
1424 self.treestates[tree] = {}
1426 self.treestates[tree][key] = val
1428 def set_tree_states(self, tree, val):
1429 self.load_tree_states()
1430 self.treestates[tree] = val
1432 def save_tree_states(self):
1433 config.user.save_file("treestates", self.treestates)
1435 def load_tree_states(self):
1436 if self.treestates is None:
1437 self.treestates = config.user.load_file("treestates", {})
1439 def finalize(self):
1440 """Finish the HTTP request processing before handing over to the application server"""
1441 self.transaction_manager.store_new()
1442 self.disable_request_timeout()
1445 # Messages
1448 def show_info(self, msg):
1449 self.message(msg, 'message')
1451 def show_error(self, msg):
1452 self.message(msg, 'error')
1454 def show_warning(self, msg):
1455 self.message(msg, 'warning')
1457 def render_info(self, msg):
1458 return self.render_message(msg, 'message')
1460 def render_error(self, msg):
1461 return self.render_message(msg, 'error')
1463 def render_warning(self, msg):
1464 return self.render_message(msg, 'warning')
1466 def message(self, msg, what='message'):
1467 self.write(self.render_message(msg, what))
1469 # obj might be either a string (str or unicode) or an exception object
1470 def render_message(self, msg, what='message'):
1471 if what == 'message':
1472 cls = 'success'
1473 prefix = _('MESSAGE')
1474 elif what == 'warning':
1475 cls = 'warning'
1476 prefix = _('WARNING')
1477 else:
1478 cls = 'error'
1479 prefix = _('ERROR')
1481 code = ""
1483 if self.output_format == "html":
1484 code += self.render_div(self.render_text(msg), class_=cls)
1485 if self.mobile:
1486 code += self.render_center(code)
1487 else:
1488 code += self.render_text('%s: %s\n' % (prefix, self.strip_tags(msg)))
1490 return code
1492 def show_localization_hint(self):
1493 url = "wato.py?mode=edit_configvar&varname=user_localizations"
1494 self.message(
1495 self.render_sup("*") + _("These texts may be localized depending on the users' "
1496 "language. You can configure the localizations %s.") %
1497 self.render_a("in the global settings", href=url))
1499 def del_language_cookie(self):
1500 self.response.delete_cookie("language")
1502 def set_language_cookie(self, lang):
1503 # type: (str) -> None
1504 cookie_lang = self.request.cookie("language")
1505 if cookie_lang != lang:
1506 if lang is not None:
1507 self.response.set_http_cookie("language", lang)
1508 else:
1509 self.del_language_cookie()
1511 def help(self, text):
1512 self.write_html(self.render_help(text))
1514 # Embed help box, whose visibility is controlled by a global button in the page.
1515 def render_help(self, text):
1516 if text and text.strip():
1517 self.have_help = True
1518 style = "display: %s;" % ("block" if self.help_visible else "none")
1519 c = self.render_div(text.strip(), class_="help", style=style)
1520 return c
1521 return ""
1524 # Debugging, diagnose and logging
1527 def debug(self, *x):
1528 for element in x:
1529 try:
1530 formatted = pprint.pformat(element)
1531 except UnicodeDecodeError:
1532 formatted = repr(element)
1533 self.write(self.render_pre(formatted))
1536 # URL building
1539 # [('varname1', value1), ('varname2', value2) ]
1540 def makeuri(self, addvars, remove_prefix=None, filename=None, delvars=None):
1541 new_vars = [nv[0] for nv in addvars]
1542 vars_ = [(v, val)
1543 for v, val in self.request.itervars()
1544 if v[0] != "_" and v not in new_vars and (not delvars or v not in delvars)]
1545 if remove_prefix is not None:
1546 vars_ = [i for i in vars_ if not i[0].startswith(remove_prefix)]
1547 vars_ = vars_ + addvars
1548 if filename is None:
1549 filename = self.urlencode(self.myfile) + ".py"
1550 if vars_:
1551 return filename + "?" + self.urlencode_vars(vars_)
1552 return filename
1554 def makeuri_contextless(self, vars_, filename=None):
1555 if not filename:
1556 filename = self.myfile + ".py"
1557 if vars_:
1558 return filename + "?" + self.urlencode_vars(vars_)
1559 return filename
1561 def makeactionuri(self, addvars, filename=None, delvars=None):
1562 return self.makeuri(
1563 addvars + [("_transid", self.transaction_manager.get())],
1564 filename=filename,
1565 delvars=delvars)
1567 def makeactionuri_contextless(self, addvars, filename=None):
1568 return self.makeuri_contextless(
1569 addvars + [("_transid", self.transaction_manager.get())], filename=filename)
1572 # HTML heading and footer rendering
1575 def default_html_headers(self):
1576 self.meta(httpequiv="Content-Type", content="text/html; charset=utf-8")
1577 self.meta(httpequiv="X-UA-Compatible", content="IE=edge")
1578 self.write_html(
1579 self._render_opening_tag(
1580 'link',
1581 rel="shortcut icon",
1582 href=self._detect_themed_image_path("images/favicon.ico"),
1583 type_="image/ico",
1584 close_tag=True))
1586 def _head(self, title, javascripts=None, stylesheets=None):
1588 javascripts = javascripts if javascripts else []
1589 stylesheets = stylesheets if stylesheets else ["pages"]
1591 self.open_head()
1593 self.default_html_headers()
1594 self.title(title)
1596 # If the variable _link_target is set, then all links in this page
1597 # should be targetted to the HTML frame named by _link_target. This
1598 # is e.g. useful in the dash-board
1599 if self.link_target:
1600 self.base(target=self.link_target)
1602 # Load all specified style sheets and all user style sheets in htdocs/css
1603 for css in self._default_stylesheets + stylesheets:
1604 fname = self._css_filename_for_browser(css)
1605 if fname is not None:
1606 self.stylesheet(fname)
1608 # write css for internet explorer
1609 fname = self._css_filename_for_browser("ie")
1610 if fname is not None:
1611 self.write_html("<!--[if IE]>\n")
1612 self.stylesheet(fname)
1613 self.write_html("<![endif]-->\n")
1615 self._add_custom_style_sheet()
1617 # Load all scripts
1618 for js in self._default_javascripts + javascripts:
1619 filename_for_browser = self.javascript_filename_for_browser(js)
1620 if filename_for_browser:
1621 self.javascript_file(filename_for_browser)
1623 if self.browser_reload != 0:
1624 if self.browser_redirect != '':
1625 self.javascript('cmk.utils.set_reload(%s, \'%s\')' % (self.browser_reload,
1626 self.browser_redirect))
1627 else:
1628 self.javascript('cmk.utils.set_reload(%s)' % (self.browser_reload))
1630 self.close_head()
1632 def _add_custom_style_sheet(self):
1633 for css in self._plugin_stylesheets():
1634 self.write('<link rel="stylesheet" type="text/css" href="css/%s">\n' % css)
1636 if config.custom_style_sheet:
1637 self.write(
1638 '<link rel="stylesheet" type="text/css" href="%s">\n' % config.custom_style_sheet)
1640 if self._theme and self._theme != "classic":
1641 fname = self._css_filename_for_browser("themes/%s/theme" % self._theme)
1642 self.stylesheet(fname)
1644 elif cmk.is_managed_edition():
1645 import cmk.gui.cme.gui_colors as gui_colors
1646 gui_colors.GUIColors().render_html()
1648 def _plugin_stylesheets(self):
1649 plugin_stylesheets = set([])
1650 for directory in [
1651 cmk.utils.paths.web_dir + "/htdocs/css",
1652 cmk.utils.paths.local_web_dir + "/htdocs/css",
1654 if os.path.exists(directory):
1655 for fn in os.listdir(directory):
1656 if fn.endswith(".css"):
1657 plugin_stylesheets.add(fn)
1658 return plugin_stylesheets
1660 # Make the browser load specified javascript files. We have some special handling here:
1661 # a) files which can not be found shal not be loaded
1662 # b) in OMD environments, add the Check_MK version to the version (prevents update problems)
1663 # c) load the minified javascript when not in debug mode
1664 def javascript_filename_for_browser(self, jsname):
1665 filename_for_browser = None
1666 rel_path = "/share/check_mk/web/htdocs/js"
1667 if self.enable_debug:
1668 min_parts = ["", "_min"]
1669 else:
1670 min_parts = ["_min", ""]
1672 for min_part in min_parts:
1673 path_pattern = cmk.utils.paths.omd_root + "%s" + rel_path + "/" + jsname + min_part + ".js"
1674 if os.path.exists(path_pattern % "") or os.path.exists(path_pattern % "/local"):
1675 filename_for_browser = 'js/%s%s-%s.js' % (jsname, min_part, cmk.__version__)
1676 break
1678 return filename_for_browser
1680 def _css_filename_for_browser(self, css):
1681 rel_path = "/share/check_mk/web/htdocs/" + css + ".css"
1682 if os.path.exists(cmk.utils.paths.omd_root + rel_path) or \
1683 os.path.exists(cmk.utils.paths.omd_root + "/local" + rel_path):
1684 return '%s-%s.css' % (css, cmk.__version__)
1686 def html_head(self, title, javascripts=None, stylesheets=None, force=False):
1688 force_new_document = force # for backward stability and better readability
1690 if force_new_document:
1691 self._header_sent = False
1693 if not self._header_sent:
1694 self.write_html('<!DOCTYPE HTML>\n')
1695 self.open_html()
1696 self._head(title, javascripts, stylesheets)
1697 self._header_sent = True
1699 def header(self,
1700 title='',
1701 javascripts=None,
1702 stylesheets=None,
1703 force=False,
1704 show_body_start=True,
1705 show_top_heading=True):
1706 if self.output_format == "html":
1707 if not self._header_sent:
1708 if show_body_start:
1709 self.body_start(
1710 title, javascripts=javascripts, stylesheets=stylesheets, force=force)
1712 self._header_sent = True
1714 if self.render_headfoot and show_top_heading:
1715 self.top_heading(title)
1717 def body_start(self, title='', javascripts=None, stylesheets=None, force=False):
1718 self.html_head(title, javascripts, stylesheets, force)
1719 self.open_body(class_=self._get_body_css_classes())
1721 def _get_body_css_classes(self):
1722 if self.screenshotmode:
1723 return self._body_classes + ["screenshotmode"]
1724 return self._body_classes
1726 def html_foot(self):
1727 self.close_html()
1729 def top_heading(self, title):
1730 if not isinstance(config.user, config.LoggedInNobody):
1731 login_text = "<b>%s</b> (%s" % (config.user.id, "+".join(config.user.role_ids))
1732 if self.enable_debug:
1733 if config.user.language():
1734 login_text += "/%s" % config.user.language()
1735 login_text += ')'
1736 else:
1737 login_text = _("not logged in")
1738 self.top_heading_left(title)
1740 self.write('<td style="min-width:240px" class=right><span id=headinfo></span>%s &nbsp; ' %
1741 login_text)
1742 if config.pagetitle_date_format:
1743 self.write(' &nbsp; <b id=headerdate format="%s"></b>' % config.pagetitle_date_format)
1744 self.write(' <b id=headertime></b>')
1745 self.top_heading_right()
1747 def top_heading_left(self, title):
1748 self.open_table(class_="header")
1749 self.open_tr()
1750 self.open_td(width="*", class_="heading")
1751 # HTML() is needed here to prevent a double escape when we do self._escape_attribute
1752 # here and self.a() escapes the content (with permissive escaping) again. We don't want
1753 # to handle "title" permissive.
1754 title = HTML(self.escaper.escape_attribute(title))
1755 self.a(
1756 title,
1757 href="#",
1758 onfocus="if (this.blur) this.blur();",
1759 onclick="this.innerHTML=\'%s\'; document.location.reload();" % _("Reloading..."))
1760 self.close_td()
1762 def top_heading_right(self):
1763 cssclass = "active" if self.help_visible else "passive"
1765 self.icon_button(
1766 None,
1767 _("Toggle context help texts"),
1768 "help",
1769 id_="helpbutton",
1770 onclick="cmk.help.toggle()",
1771 style="display:none",
1772 cssclass=cssclass)
1773 self.open_a(href="https://mathias-kettner.com", class_="head_logo")
1774 self.img(src=self._detect_themed_image_path("images/logo_cmk_small.png"))
1775 self.close_a()
1776 self.close_td()
1777 self.close_tr()
1778 self.close_table()
1779 self.hr(class_="header")
1781 if self.enable_debug:
1782 self._dump_get_vars()
1784 def footer(self, show_footer=True, show_body_end=True):
1785 if self.output_format == "html":
1786 if show_footer:
1787 self.bottom_footer()
1789 if show_body_end:
1790 self.body_end()
1792 def bottom_footer(self):
1793 if self._header_sent:
1794 self.bottom_focuscode()
1795 if self.render_headfoot:
1796 self.open_table(class_="footer")
1797 self.open_tr()
1799 self.open_td(class_="left")
1800 self._write_status_icons()
1801 self.close_td()
1803 self.td('', class_="middle")
1805 self.open_td(class_="right")
1806 content = _("refresh: %s secs") % self.render_div(
1807 self.browser_reload, id_="foot_refresh_time")
1808 style = "display:inline-block;" if self.browser_reload else "display:none;"
1809 self.div(HTML(content), id_="foot_refresh", style=style)
1810 self.close_td()
1812 self.close_tr()
1813 self.close_table()
1815 def bottom_focuscode(self):
1816 if self.focus_object:
1817 if isinstance(self.focus_object, tuple):
1818 formname, varname = self.focus_object
1819 obj_ident = formname + "." + varname
1820 else:
1821 obj_ident = "getElementById(\"%s\")" % self.focus_object
1823 js_code = "<!--\n" \
1824 "var focus_obj = document.%s;\n" \
1825 "if (focus_obj) {\n" \
1826 " focus_obj.focus();\n" \
1827 " if (focus_obj.select)\n" \
1828 " focus_obj.select();\n" \
1829 "}\n" \
1830 "// -->\n" % obj_ident
1831 self.javascript(js_code)
1833 def focus_here(self):
1834 self.a("", href="#focus_me", id_="focus_me")
1835 self.set_focus_by_id("focus_me")
1837 def body_end(self):
1838 if self.have_help:
1839 self.javascript("cmk.help.enable();")
1840 if self.final_javascript_code:
1841 self.javascript(self.final_javascript_code)
1842 self.javascript("cmk.visibility_detection.initialize();")
1843 self.close_body()
1844 self.close_html()
1847 # HTML form rendering
1850 def begin_form(self, name, action=None, method="GET", onsubmit=None, add_transid=True):
1851 self.form_vars = []
1852 if action is None:
1853 action = self.myfile + ".py"
1854 self.current_form = name
1855 self.open_form(
1856 id_="form_%s" % name,
1857 name=name,
1858 class_=name,
1859 action=action,
1860 method=method,
1861 onsubmit=onsubmit,
1862 enctype="multipart/form-data" if method.lower() == "post" else None)
1863 self.hidden_field("filled_in", name, add_var=True)
1864 if add_transid:
1865 self.hidden_field("_transid", str(self.transaction_manager.get()))
1866 self.form_name = name
1868 def end_form(self):
1869 self.close_form()
1870 self.form_name = None
1872 def in_form(self):
1873 return self.form_name is not None
1875 def prevent_password_auto_completion(self):
1876 # These fields are not really used by the form. They are used to prevent the browsers
1877 # from filling the default password and previous input fields in the form
1878 # with password which are eventually saved in the browsers password store.
1879 self.input(name=None, type_="text", style="display:none;")
1880 self.input(name=None, type_="password", style="display:none;")
1882 # Needed if input elements are put into forms without the helper
1883 # functions of us. TODO: Should really be removed and cleaned up!
1884 def add_form_var(self, varname):
1885 self.form_vars.append(varname)
1887 # Beware: call this method just before end_form(). It will
1888 # add all current non-underscored HTML variables as hiddedn
1889 # field to the form - *if* they are not used in any input
1890 # field. (this is the reason why you must not add any further
1891 # input fields after this method has been called).
1892 def hidden_fields(self, varlist=None, **args):
1893 add_action_vars = args.get("add_action_vars", False)
1894 if varlist is not None:
1895 for var in varlist:
1896 self.hidden_field(var, self.request.var(var, ""))
1897 else: # add *all* get variables, that are not set by any input!
1898 for var, _val in self.request.itervars():
1899 if var not in self.form_vars and \
1900 (var[0] != "_" or add_action_vars): # and var != "filled_in":
1901 self.hidden_field(var, self.get_unicode_input(var))
1903 def hidden_field(self, var, value, id_=None, add_var=False, class_=None):
1904 self.write_html(
1905 self.render_hidden_field(var=var, value=value, id_=id_, add_var=add_var, class_=class_))
1907 def render_hidden_field(self, var, value, id_=None, add_var=False, class_=None):
1908 if value is None:
1909 return ""
1910 if add_var:
1911 self.add_form_var(var)
1912 return self.render_input(name=var, type_="hidden", id_=id_, value=value, class_=class_)
1915 # Form submission and variable handling
1918 def do_actions(self):
1919 return self.request.var("_do_actions") not in ["", None, _("No")]
1921 # Check if the given form is currently filled in (i.e. we display
1922 # the form a second time while showing value typed in at the first
1923 # time and complaining about invalid user input)
1924 def form_submitted(self, form_name=None):
1925 if form_name is None:
1926 return self.request.has_var("filled_in")
1927 return self.request.var("filled_in") == form_name
1929 # Get value of checkbox. Return True, False or None. None means
1930 # that no form has been submitted. The problem here is the distintion
1931 # between False and None. The browser does not set the variables for
1932 # Checkboxes that are not checked :-(
1933 def get_checkbox(self, varname):
1934 if self.request.has_var(varname):
1935 return bool(self.request.var(varname))
1936 if self.form_submitted(self.form_name):
1937 return False # Form filled in but variable missing -> Checkbox not checked
1938 return None
1941 # Button elements
1944 def button(self, varname, title, cssclass=None, style=None, help_=None):
1945 self.write_html(self.render_button(varname, title, cssclass, style, help_=help_))
1947 def render_button(self, varname, title, cssclass=None, style=None, help_=None):
1948 self.add_form_var(varname)
1949 return self.render_input(
1950 name=varname,
1951 type_="submit",
1952 id_=varname,
1953 class_=["button", cssclass if cssclass else None],
1954 value=title,
1955 title=help_,
1956 style=style)
1958 def buttonlink(self,
1959 href,
1960 text,
1961 add_transid=False,
1962 obj_id=None,
1963 style=None,
1964 title=None,
1965 disabled=None,
1966 class_=None):
1967 if add_transid:
1968 href += "&_transid=%s" % self.transaction_manager.get()
1970 if not obj_id:
1971 obj_id = utils.gen_id()
1973 # Same API as other elements: class_ can be a list or string/None
1974 css_classes = ["button", "buttonlink"]
1975 if class_:
1976 if not isinstance(class_, list):
1977 css_classes.append(class_)
1978 else:
1979 css_classes.extend(class_)
1981 self.input(
1982 name=obj_id,
1983 type_="button",
1984 id_=obj_id,
1985 class_=css_classes,
1986 value=text,
1987 style=style,
1988 title=title,
1989 disabled=disabled,
1990 onclick="location.href=\'%s\'" % href)
1992 # TODO: Refactor the arguments. It is only used in views/wato
1993 def toggle_button(self,
1994 id_,
1995 isopen,
1996 icon,
1997 title,
1998 hidden=False,
1999 disabled=False,
2000 onclick=None,
2001 is_context_button=True):
2002 if is_context_button:
2003 self.begin_context_buttons() # TODO: Check all calls. If done before, remove this!
2005 if not onclick and not disabled:
2006 onclick = "cmk.views.toggle_form(this.parentNode, '%s');" % id_
2008 if disabled:
2009 state = "off" if disabled else "on"
2010 cssclass = ""
2011 title = ""
2012 else:
2013 state = "on"
2014 if isopen:
2015 cssclass = "down"
2016 else:
2017 cssclass = "up"
2019 self.open_div(
2020 id_="%s_%s" % (id_, state),
2021 class_=["togglebutton", state, icon, cssclass],
2022 title=title,
2023 style='display:none' if hidden else None,
2025 self.open_a("javascript:void(0)", onclick=onclick)
2026 self.icon(title=None, icon=icon)
2027 self.close_a()
2028 self.close_div()
2030 def get_button_counts(self):
2031 return config.user.get_button_counts()
2033 def empty_icon_button(self):
2034 self.write(self.render_icon("images/trans.png", cssclass="iconbutton trans"))
2036 def disabled_icon_button(self, icon):
2037 self.write(self.render_icon(icon, cssclass="iconbutton"))
2039 # TODO: Cleanup to use standard attributes etc.
2040 def jsbutton(self,
2041 varname,
2042 text,
2043 onclick,
2044 style='',
2045 cssclass="",
2046 title="",
2047 disabled=False,
2048 class_=None):
2049 # Same API as other elements: class_ can be a list or string/None
2050 classes = []
2051 if class_:
2052 classes = class_ if isinstance(class_, list) else [class_]
2054 if disabled:
2055 classes.append("disabled")
2056 disabled = ""
2057 else:
2058 disabled = None
2060 # autocomplete="off": Is needed for firefox not to set "disabled="disabled" during page reload
2061 # when it has been set on a page via javascript before. Needed for WATO activate changes page.
2062 self.input(
2063 name=varname,
2064 type_="button",
2065 id_=varname,
2066 class_=["button", cssclass] + classes,
2067 autocomplete="off",
2068 onclick=onclick,
2069 style=style,
2070 disabled=disabled,
2071 value=text,
2072 title=title)
2075 # Other input elements
2078 def user_error(self, e):
2079 assert isinstance(e, MKUserError), "ERROR: This exception is not a user error!"
2080 self.open_div(class_="error")
2081 self.write("%s" % e.message)
2082 self.close_div()
2083 self.add_user_error(e.varname, e)
2085 # user errors are used by input elements to show invalid input
2086 def add_user_error(self, varname, msg_or_exc):
2087 if isinstance(msg_or_exc, Exception):
2088 message = "%s" % msg_or_exc
2089 else:
2090 message = msg_or_exc
2092 if isinstance(varname, list):
2093 for v in varname:
2094 self.add_user_error(v, message)
2095 else:
2096 self.user_errors[varname] = message
2098 def has_user_errors(self):
2099 return len(self.user_errors) > 0
2101 def show_user_errors(self):
2102 if self.has_user_errors():
2103 self.open_div(class_="error")
2104 self.write('<br>'.join(self.user_errors.values()))
2105 self.close_div()
2107 def text_input(self,
2108 varname,
2109 default_value="",
2110 cssclass="text",
2111 label=None,
2112 id_=None,
2113 submit=None,
2114 attrs=None,
2115 **args):
2116 if attrs is None:
2117 attrs = {}
2119 # Model
2120 error = self.user_errors.get(varname)
2121 value = self.request.var(varname, default_value)
2122 if not value:
2123 value = ""
2124 if error:
2125 self.set_focus(varname)
2126 self.form_vars.append(varname)
2128 # View
2129 style, size = None, None
2130 if args.get("try_max_width"):
2131 style = "width: calc(100% - 10px); "
2132 if "size" in args:
2133 cols = int(args["size"])
2134 else:
2135 cols = 16
2136 style += "min-width: %d.8ex; " % cols
2138 elif "size" in args and args["size"]:
2139 if args["size"] == "max":
2140 style = "width: 100%;"
2141 else:
2142 size = "%d" % (args["size"] + 1)
2143 if not args.get('omit_css_width', False) and "width:" not in args.get(
2144 "style", "") and not self.mobile:
2145 style = "width: %d.8ex;" % args["size"]
2147 if args.get("style"):
2148 style = [style, args["style"]]
2150 if (submit or label) and not id_:
2151 id_ = "ti_%s" % varname
2153 onkeydown = None if not submit else HTML(
2154 'cmk.forms.textinput_enter_submit(event, \'%s\');' % (submit))
2156 attributes = {
2157 "class": cssclass,
2158 "id": id_,
2159 "style": style,
2160 "size": size,
2161 "autocomplete": args.get("autocomplete"),
2162 "readonly": "true" if args.get("read_only") else None,
2163 "value": value,
2164 "onkeydown": onkeydown,
2167 for key, val in attrs.iteritems():
2168 if key not in attributes and key not in ["name", "type", "type_"]:
2169 attributes[key] = val
2170 elif key in attributes and attributes[key] is None:
2171 attributes[key] = val
2173 if error:
2174 self.open_x(class_="inputerror")
2176 if label:
2177 self.label(label, for_=id_)
2178 self.write_html(self.render_input(varname, type_=args.get("type_", "text"), **attributes))
2180 if error:
2181 self.close_x()
2183 # Shows a colored badge with text (used on WATO activation page for the site status)
2184 def status_label(self, content, status, title, **attrs):
2185 self.status_label_button(content, status, title, onclick=None, **attrs)
2187 # Shows a colored button with text (used in site and customer status snapins)
2188 def status_label_button(self, content, status, title, onclick, **attrs):
2189 button_cls = "button" if onclick else None
2190 self.div(
2191 content,
2192 title=title,
2193 class_=["status_label", button_cls, status],
2194 onclick=onclick,
2195 **attrs)
2197 def toggle_switch(self, enabled, help_txt, **attrs):
2198 # Same API as other elements: class_ can be a list or string/None
2199 if "class_" in attrs:
2200 if not isinstance(attrs["class_"], list):
2201 attrs["class_"] = [attrs["class_"]]
2202 else:
2203 attrs["class_"] = []
2205 attrs["class_"] += [
2206 "toggle_switch",
2207 "on" if enabled else "off",
2210 link_attrs = {
2211 "href": attrs.pop("href", "javascript:void(0)"),
2212 "onclick": attrs.pop("onclick", None),
2215 self.open_div(**attrs)
2216 self.a(_("on") if enabled else _("off"), title=help_txt, **link_attrs)
2217 self.close_div()
2219 def number_input(self, varname, deflt="", size=8, style="", submit=None):
2220 if deflt is not None:
2221 deflt = str(deflt)
2222 self.text_input(varname, deflt, "number", size=size, style=style, submit=submit)
2224 def password_input(self, varname, default_value="", size=12, **args):
2225 self.text_input(varname, default_value, type_="password", size=size, **args)
2227 def text_area(self, varname, deflt="", rows=4, cols=30, attrs=None, try_max_width=False):
2228 if attrs is None:
2229 attrs = {}
2231 # Model
2232 value = self.request.var(varname, deflt)
2233 error = self.user_errors.get(varname)
2235 self.form_vars.append(varname)
2236 if error:
2237 self.set_focus(varname)
2239 # View
2240 style = "width: %d.8ex;" % cols
2241 if try_max_width:
2242 style += "width: calc(100%% - 10px); min-width: %d.8ex;" % cols
2243 attrs["style"] = style
2244 attrs["rows"] = rows
2245 attrs["cols"] = cols
2246 attrs["name"] = varname
2248 # Fix handling of leading newlines (https://www.w3.org/TR/html5/syntax.html#element-restrictions)
2250 # """
2251 # A single newline may be placed immediately after the start tag of pre
2252 # and textarea elements. This does not affect the processing of the
2253 # element. The otherwise optional newline must be included if the
2254 # element’s contents themselves start with a newline (because otherwise
2255 # the leading newline in the contents would be treated like the
2256 # optional newline, and ignored).
2257 # """
2258 if value and value.startswith("\n"):
2259 value = "\n" + value
2261 if error:
2262 self.open_x(class_="inputerror")
2263 self.write_html(self._render_content_tag("textarea", value, **attrs))
2264 if error:
2265 self.close_x()
2267 # TODO: DEPRECATED!!
2268 def sorted_select(self, varname, choices, deflt='', onchange=None, attrs=None):
2269 if attrs is None:
2270 attrs = {}
2271 self.dropdown(varname, choices, deflt=deflt, onchange=onchange, ordered=True, **attrs)
2273 # TODO: DEPRECATED!!
2274 def select(self, varname, choices, deflt='', onchange=None, attrs=None):
2275 if attrs is None:
2276 attrs = {}
2277 self.dropdown(varname, choices, deflt=deflt, onchange=onchange, **attrs)
2279 # TODO: DEPRECATED!!
2280 def icon_select(self, varname, choices, deflt=''):
2281 self.icon_dropdown(varname, choices, deflt=deflt)
2283 # Choices is a list pairs of (key, title). They keys of the choices
2284 # and the default value must be of type None, str or unicode.
2285 def dropdown(self, varname, choices, deflt='', ordered=False, **attrs):
2287 current = self.get_unicode_input(varname, deflt)
2288 error = self.user_errors.get(varname)
2289 if varname:
2290 self.form_vars.append(varname)
2291 attrs.setdefault('size', 1)
2293 chs = choices[:]
2294 if ordered:
2295 # Sort according to display texts, not keys
2296 chs.sort(key=lambda a: a[1].lower())
2298 if error:
2299 self.open_x(class_="inputerror")
2301 if "read_only" in attrs and attrs.pop("read_only"):
2302 attrs["disabled"] = "disabled"
2303 self.hidden_field(varname, current, add_var=False)
2305 if attrs.get("label"):
2306 self.label(attrs["label"], for_=varname)
2308 # Do not enable select2 for select fields that allow multiple
2309 # selections like the dual list choice valuespec
2310 if "multiple" not in attrs:
2311 if "class_" in attrs:
2312 if isinstance(attrs["class_"], list):
2313 attrs["class_"].insert(0, "select2-enable")
2314 else:
2315 attrs["class_"] = ["select2-enable", attrs["class_"]]
2316 else:
2317 attrs["class_"] = ["select2-enable"]
2319 self.open_select(name=varname, id_=varname, **attrs)
2320 for value, text in chs:
2321 # if both the default in choices and current was '' then selected depended on the order in choices
2322 selected = (value == current) or (not value and not current)
2323 self.option(text, value=value if value else "", selected="" if selected else None)
2324 self.close_select()
2325 if error:
2326 self.close_x()
2328 def icon_dropdown(self, varname, choices, deflt=""):
2329 current = self.request.var(varname, deflt)
2330 if varname:
2331 self.form_vars.append(varname)
2333 self.open_select(class_="icon", name=varname, id_=varname, size="1")
2334 for value, text, icon in choices:
2335 # if both the default in choices and current was '' then selected depended on the order in choices
2336 selected = (value == current) or (not value and not current)
2337 self.option(
2338 text,
2339 value=value if value else "",
2340 selected='' if selected else None,
2341 style="background-image:url(images/icon_%s.png);" % icon)
2342 self.close_select()
2344 # Wrapper for DualListChoice
2345 def multi_select(self, varname, choices, deflt='', ordered='', **attrs):
2346 attrs["multiple"] = "multiple"
2347 self.dropdown(varname, choices, deflt=deflt, ordered=ordered, **attrs)
2349 def upload_file(self, varname):
2350 error = self.user_errors.get(varname)
2351 if error:
2352 self.open_x(class_="inputerror")
2353 self.input(name=varname, type_="file")
2354 if error:
2355 self.close_x()
2356 self.form_vars.append(varname)
2358 # The confirm dialog is normally not a dialog which need to be protected
2359 # by a transid itselfs. It is only a intermediate step to the real action
2360 # But there are use cases where the confirm dialog is used during rendering
2361 # a normal page, for example when deleting a dashlet from a dashboard. In
2362 # such cases, the transid must be added by the confirm dialog.
2363 # add_header: A title can be given to make the confirm method render the HTML
2364 # header when showing the confirm message.
2365 def confirm(self, msg, method="POST", action=None, add_transid=False, add_header=False):
2366 if self.request.var("_do_actions") == _("No"):
2367 # User has pressed "No", now invalidate the unused transid
2368 self.check_transaction()
2369 return # None --> "No"
2371 if not self.request.has_var("_do_confirm"):
2372 if add_header != False:
2373 self.header(add_header)
2375 if self.mobile:
2376 self.open_center()
2377 self.open_div(class_="really")
2378 self.write_text(msg)
2379 # FIXME: When this confirms another form, use the form name from self.request.itervars()
2380 self.begin_form("confirm", method=method, action=action, add_transid=add_transid)
2381 self.hidden_fields(add_action_vars=True)
2382 self.button("_do_confirm", _("Yes!"), "really")
2383 self.button("_do_actions", _("No"), "")
2384 self.end_form()
2385 self.close_div()
2386 if self.mobile:
2387 self.close_center()
2389 return False # False --> "Dialog shown, no answer yet"
2390 else:
2391 # Now check the transaction
2392 return True if self.check_transaction(
2393 ) else None # True: "Yes", None --> Browser reload of "yes" page
2396 # Radio groups
2399 def begin_radio_group(self, horizontal=False):
2400 if self.mobile:
2401 attrs = {'data-type': "horizontal" if horizontal else None, 'data-role': "controlgroup"}
2402 self.write(self._render_opening_tag("fieldset", **attrs))
2404 def end_radio_group(self):
2405 if self.mobile:
2406 self.write(self._render_closing_tag("fieldset"))
2408 def radiobutton(self, varname, value, checked, label):
2409 # Model
2410 self.form_vars.append(varname)
2412 # Controller
2413 if self.request.has_var(varname):
2414 checked = self.request.var(varname) == value
2416 # View
2417 id_ = "rb_%s_%s" % (varname, value) if label else None
2418 self.open_span(class_="radiobutton_group")
2419 self.input(
2420 name=varname, type_="radio", value=value, checked='' if checked else None, id_=id_)
2421 if label:
2422 self.label(label, for_=id_)
2423 self.close_span()
2426 # Checkbox groups
2429 def begin_checkbox_group(self, horizonal=False):
2430 self.begin_radio_group(horizonal)
2432 def end_checkbox_group(self):
2433 self.end_radio_group()
2435 def checkbox(self, *args, **kwargs):
2436 self.write(self.render_checkbox(*args, **kwargs))
2438 def render_checkbox(self, varname, deflt=False, label='', id_=None, **add_attr):
2440 # Problem with checkboxes: The browser will add the variable
2441 # only to the URL if the box is checked. So in order to detect
2442 # whether we should add the default value, we need to detect
2443 # if the form is printed for the first time. This is the
2444 # case if "filled_in" is not set.
2445 value = self.get_checkbox(varname)
2446 if value is None: # form not yet filled in
2447 value = deflt
2449 error = self.user_errors.get(varname)
2450 if id_ is None:
2451 id_ = "cb_%s" % varname
2453 add_attr["id"] = id_
2454 add_attr["CHECKED"] = '' if value else None
2456 code = self.render_input(name=varname, type_="checkbox", **add_attr)\
2457 + self.render_label(label, for_=id_)
2458 code = self.render_span(code, class_="checkbox")
2460 if error:
2461 code = self.render_x(code, class_="inputerror")
2463 self.form_vars.append(varname)
2464 return code
2467 # Foldable context
2470 def begin_foldable_container(self,
2471 treename,
2472 id_,
2473 isopen,
2474 title,
2475 indent=True,
2476 first=False,
2477 icon=None,
2478 fetch_url=None,
2479 title_url=None,
2480 tree_img="tree"):
2481 self.folding_indent = indent
2483 if self._user_id:
2484 isopen = self.foldable_container_is_open(treename, id_, isopen)
2486 onclick = "cmk.foldable_container.toggle(%s, %s, %s)"\
2487 % (json.dumps(treename), json.dumps(id_), json.dumps(fetch_url if fetch_url else ''))
2489 img_id = "treeimg.%s.%s" % (treename, id_)
2491 if indent == "nform":
2492 self.open_tr(class_="heading")
2493 self.open_td(id_="nform.%s.%s" % (treename, id_), onclick=onclick, colspan="2")
2494 if icon:
2495 self.img(class_=["treeangle", "title"], src="images/icon_%s.png" % icon)
2496 else:
2497 self.img(
2498 id_=img_id,
2499 class_=["treeangle", "nform", "open" if isopen else "closed"],
2500 src="images/%s_closed.png" % tree_img,
2501 align="absbottom")
2502 self.write_text(title)
2503 self.close_td()
2504 self.close_tr()
2505 else:
2506 self.open_div(class_="foldable")
2508 if not icon:
2509 self.img(
2510 id_="treeimg.%s.%s" % (treename, id_),
2511 class_=["treeangle", "open" if isopen else "closed"],
2512 src="images/%s_closed.png" % tree_img,
2513 align="absbottom",
2514 onclick=onclick)
2515 if isinstance(title, HTML): # custom HTML code
2516 if icon:
2517 self.img(
2518 class_=["treeangle", "title"],
2519 src="images/icon_%s.png" % icon,
2520 onclick=onclick)
2521 self.write_text(title)
2522 if indent != "form":
2523 self.br()
2524 else:
2525 self.open_b(class_=["treeangle", "title"], onclick=None if title_url else onclick)
2526 if icon:
2527 self.img(class_=["treeangle", "title"], src="images/icon_%s.png" % icon)
2528 if title_url:
2529 self.a(title, href=title_url)
2530 else:
2531 self.write_text(title)
2532 self.close_b()
2533 self.br()
2535 indent_style = "padding-left: %dpx; " % (indent is True and 15 or 0)
2536 if indent == "form":
2537 self.close_td()
2538 self.close_tr()
2539 self.close_table()
2540 indent_style += "margin: 0; "
2541 self.open_ul(
2542 id_="tree.%s.%s" % (treename, id_),
2543 class_=["treeangle", "open" if isopen else "closed"],
2544 style=indent_style)
2546 # give caller information about current toggling state (needed for nform)
2547 return isopen
2549 def end_foldable_container(self):
2550 if self.folding_indent != "nform":
2551 self.close_ul()
2552 self.close_div()
2554 def foldable_container_is_open(self, treename, id_, isopen):
2555 # try to get persisted state of tree
2556 tree_state = self.get_tree_states(treename)
2558 if id_ in tree_state:
2559 isopen = tree_state[id_] == "on"
2560 return isopen
2563 # Context Buttons
2566 def begin_context_buttons(self):
2567 if not self._context_buttons_open:
2568 self.context_button_hidden = False
2569 self.open_div(class_="contextlinks")
2570 self._context_buttons_open = True
2572 def end_context_buttons(self):
2573 if self._context_buttons_open:
2574 if self.context_button_hidden:
2575 self.open_div(
2576 title=_("Show all buttons"), id="toggle", class_=["contextlink", "short"])
2577 self.a("...", onclick='cmk.utils.unhide_context_buttons(this);', href='#')
2578 self.close_div()
2579 self.div("", class_="end")
2580 self.close_div()
2581 self._context_buttons_open = False
2583 def context_button(self,
2584 title,
2585 url,
2586 icon=None,
2587 hot=False,
2588 id_=None,
2589 bestof=None,
2590 hover_title=None,
2591 class_=None):
2592 self._context_button(
2593 title,
2594 url,
2595 icon=icon,
2596 hot=hot,
2597 id_=id_,
2598 bestof=bestof,
2599 hover_title=hover_title,
2600 class_=class_)
2602 def _context_button(self,
2603 title,
2604 url,
2605 icon=None,
2606 hot=False,
2607 id_=None,
2608 bestof=None,
2609 hover_title=None,
2610 class_=None):
2611 title = self.attrencode(title)
2612 display = "block"
2613 if bestof:
2614 counts = self.get_button_counts()
2615 weights = counts.items()
2616 weights.sort(cmp=lambda a, b: cmp(a[1], b[1]))
2617 best = dict(weights[-bestof:]) # pylint: disable=invalid-unary-operand-type
2618 if id_ not in best:
2619 display = "none"
2620 self.context_button_hidden = True
2622 if not self._context_buttons_open:
2623 self.begin_context_buttons()
2625 css_classes = ["contextlink"]
2626 if hot:
2627 css_classes.append("hot")
2628 if class_:
2629 if isinstance(class_, list):
2630 css_classes += class_
2631 else:
2632 css_classes += class_.split(" ")
2634 self.open_div(class_=css_classes, id_=id_, style="display:%s;" % display)
2636 self.open_a(
2637 href=url,
2638 title=hover_title,
2639 onclick="cmk.utils.count_context_button(this);" if bestof else None)
2641 if icon:
2642 self.icon('', icon, cssclass="inline", middle=False)
2644 self.span(title)
2646 self.close_a()
2648 self.close_div()
2651 # Floating Options
2654 def begin_floating_options(self, div_id, is_open):
2655 self.open_div(
2656 id_=div_id, class_=["view_form"], style="display: none" if not is_open else None)
2657 self.open_table(class_=["filterform"], cellpadding="0", cellspacing="0", border="0")
2658 self.open_tr()
2659 self.open_td()
2661 def end_floating_options(self, reset_url=None):
2662 self.close_td()
2663 self.close_tr()
2664 self.open_tr()
2665 self.open_td()
2666 self.button("apply", _("Apply"), "submit")
2667 if reset_url:
2668 self.buttonlink(reset_url, _("Reset to defaults"))
2670 self.close_td()
2671 self.close_tr()
2672 self.close_table()
2673 self.close_div()
2675 def render_floating_option(self, name, height, varprefix, valuespec, value):
2676 self.open_div(class_=["floatfilter", height, name])
2677 self.div(valuespec.title(), class_=["legend"])
2678 self.open_div(class_=["content"])
2679 valuespec.render_input(varprefix + name, value)
2680 self.close_div()
2681 self.close_div()
2684 # HTML icon rendering
2687 # FIXME: Change order of input arguments in one: icon and render_icon!!
2688 def icon(self, title, icon, **kwargs):
2690 icon_name = icon
2692 self.write_html(self.render_icon(icon_name=icon_name, title=title, **kwargs))
2694 def empty_icon(self):
2695 self.write_html(self.render_icon("images/trans.png"))
2697 def render_icon(self, icon_name, title=None, middle=True, id_=None, cssclass=None, class_=None):
2699 attributes = {
2700 'title': title,
2701 'id': id_,
2702 'class': ["icon", cssclass],
2703 'align': 'absmiddle' if middle else None,
2704 'src': icon_name if "/" in icon_name else self._detect_icon_path(icon_name)
2707 if class_:
2708 attributes['class'].extend(class_)
2710 return self._render_opening_tag('img', close_tag=True, **attributes)
2712 def _detect_icon_path(self, icon_name):
2713 """Detect from which place an icon shall be used and return it's path relative to
2714 htdocs/
2716 Priority:
2717 1. In case a theme is active: themes/images/icon_[name].png in site local hierarc
2719 2. In case a theme is active: themes/images/icon_[name].png in standard hierarchy
2720 3. images/icon_[name].png in site local hierarchy
2721 4. images/icon_[name].png in standard hierarchy
2722 5. images/icons/[name].png in site local hierarchy
2723 6. images/icons/[name].png in standard hierarchy
2726 if self._theme and self._theme != "classic":
2727 rel_path = "share/check_mk/web/htdocs/themes/%s/images/icon_%s.png" % (self._theme,
2728 icon_name)
2729 if os.path.exists(cmk.utils.paths.omd_root + "/" + rel_path) or os.path.exists(
2730 cmk.utils.paths.omd_root + "/local/" + rel_path):
2731 return "themes/%s/images/icon_%s.png" % (self._theme, icon_name)
2733 rel_path = "share/check_mk/web/htdocs/images/icon_" + icon_name + ".png"
2734 if os.path.exists(cmk.utils.paths.omd_root + "/" + rel_path) or os.path.exists(
2735 cmk.utils.paths.omd_root + "/local/" + rel_path):
2736 return "images/icon_%s.png" % icon_name
2738 return "images/icons/%s.png" % icon_name
2740 def _detect_themed_image_path(self, img_path):
2741 """Detect whether or not the original file or a themed image should be loaded by the client
2743 Priority:
2744 1. In case a theme is active: themes/[img_path] in site local hierarchy
2745 2. In case a theme is active: themes/[img_path] in standard hierarchy
2746 3. [img_path] in site local hierarchy
2747 4. [img_path] in standard hierarchy
2750 if self._theme and self._theme != "classic":
2751 rel_path = "share/check_mk/web/htdocs/themes/%s/%s" % (self._theme, img_path)
2752 if os.path.exists(cmk.utils.paths.omd_root + "/" + rel_path) or os.path.exists(
2753 cmk.utils.paths.omd_root + "/local/" + rel_path):
2754 return "themes/%s/%s" % (self._theme, img_path)
2756 return img_path
2758 def render_icon_button(self,
2759 url,
2760 title,
2761 icon,
2762 id_=None,
2763 onclick=None,
2764 style=None,
2765 target=None,
2766 cssclass=None,
2767 class_=None):
2768 # Same API as other elements: class_ can be a list or string/None
2769 classes = []
2770 if cssclass:
2771 classes.append(cssclass)
2772 if class_:
2773 classes = class_ if isinstance(class_, list) else [class_]
2775 icon = HTML(self.render_icon(icon, cssclass="iconbutton"))
2777 return self.render_a(
2778 icon, **{
2779 'title': title,
2780 'id': id_,
2781 'class': classes,
2782 'style': style,
2783 'target': target if target else '',
2784 'href': url if not onclick else "javascript:void(0)",
2785 'onfocus': "if (this.blur) this.blur();",
2786 'onclick': onclick
2789 def icon_button(self, *args, **kwargs):
2790 self.write_html(self.render_icon_button(*args, **kwargs))
2792 def popup_trigger(self, *args, **kwargs):
2793 self.write_html(self.render_popup_trigger(*args, **kwargs))
2795 def render_popup_trigger(self,
2796 content,
2797 ident,
2798 what=None,
2799 data=None,
2800 url_vars=None,
2801 style=None,
2802 menu_content=None,
2803 cssclass=None,
2804 onclose=None,
2805 resizable=False):
2807 onclick = 'cmk.popup_menu.toggle_popup(event, this, %s, %s, %s, %s, %s, %s, %s);' % \
2808 (json.dumps(ident),
2809 json.dumps(what if what else None),
2810 json.dumps(data if data else None),
2811 json.dumps(self.urlencode_vars(url_vars) if url_vars else None),
2812 json.dumps(menu_content if menu_content else None),
2813 json.dumps(onclose.replace("'", "\\'") if onclose else None),
2814 json.dumps(resizable))
2816 #TODO: Check if HTML'ing content is correct and necessary!
2817 atag = self.render_a(
2818 HTML(content), class_="popup_trigger", href="javascript:void(0);", onclick=onclick)
2820 return self.render_div(
2821 atag, class_=["popup_trigger", cssclass], id_="popup_trigger_%s" % ident, style=style)
2823 def element_dragger_url(self, dragging_tag, base_url):
2824 self.write_html(
2825 self.render_element_dragger(
2826 dragging_tag,
2827 drop_handler=
2828 "function(index){return cmk.element_dragging.url_drop_handler(%s, index);})" %
2829 json.dumps(base_url)))
2831 def element_dragger_js(self, dragging_tag, drop_handler, handler_args):
2832 self.write_html(
2833 self.render_element_dragger(
2834 dragging_tag,
2835 drop_handler="function(new_index){return %s(%s, new_index);})" %
2836 (drop_handler, json.dumps(handler_args))))
2838 # Currently only tested with tables. But with some small changes it may work with other
2839 # structures too.
2840 def render_element_dragger(self, dragging_tag, drop_handler):
2841 return self.render_a(
2842 self.render_icon("drag", _("Move this entry")),
2843 href="javascript:void(0)",
2844 class_=["element_dragger"],
2845 onmousedown="cmk.element_dragging.start(event, this, %s, %s" % (json.dumps(
2846 dragging_tag.upper()), drop_handler))
2849 # HTML - All the common and more complex HTML rendering methods
2852 def _dump_get_vars(self):
2853 self.begin_foldable_container("html", "debug_vars", True,
2854 _("GET/POST variables of this page"))
2855 self.debug_vars(hide_with_mouse=False)
2856 self.end_foldable_container()
2858 def debug_vars(self, prefix=None, hide_with_mouse=True, vars_=None):
2859 it = self.request.itervars() if vars_ is None else vars_.iteritems()
2860 hover = "this.style.display=\'none\';"
2861 self.open_table(class_=["debug_vars"], onmouseover=hover if hide_with_mouse else None)
2862 self.tr(self.render_th(_("POST / GET Variables"), colspan="2"))
2863 for name, value in sorted(it):
2864 if name in ["_password", "password"]:
2865 value = "***"
2866 if not prefix or name.startswith(prefix):
2867 self.tr(self.render_td(name, class_="left") + self.render_td(value, class_="right"))
2868 self.close_table()
2870 # TODO: Rename the status_icons because they are not only showing states. There are also actions.
2871 # Something like footer icons or similar seems to be better
2872 def _write_status_icons(self):
2873 self.icon_button(
2874 self.makeuri([]), _("URL to this frame"), "frameurl", target="_top", cssclass="inline")
2875 self.icon_button(
2876 "index.py?" + self.urlencode_vars([("start_url", self.makeuri([]))]),
2877 _("URL to this page including sidebar"),
2878 "pageurl",
2879 target="_top",
2880 cssclass="inline")
2882 # TODO: Move this away from here. Make a context button. The view should handle this
2883 if self.myfile == "view" and self.request.var('mode') != 'availability' and config.user.may(
2884 "general.csv_export"):
2885 self.icon_button(
2886 self.makeuri([("output_format", "csv_export")]),
2887 _("Export as CSV"),
2888 "download_csv",
2889 target="_top",
2890 cssclass="inline")
2892 # TODO: This needs to be realized as plugin mechanism
2893 if self.myfile == "view":
2894 mode_name = "availability" if self.request.var("mode") == "availability" else "view"
2896 encoded_vars = {}
2897 for k, v in self.page_context.items():
2898 if v is None:
2899 v = ''
2900 elif isinstance(v, unicode):
2901 v = v.encode('utf-8')
2902 encoded_vars[k] = v
2904 self.popup_trigger(
2905 self.render_icon("menu", _("Add this view to..."), cssclass="iconbutton inline"),
2906 'add_visual',
2907 'add_visual',
2908 data=[mode_name, encoded_vars, {
2909 'name': self.request.var('view_name')
2911 url_vars=[("add_type", mode_name)])
2913 # TODO: This should be handled by pagetypes.py
2914 elif self.myfile == "graph_collection":
2916 self.popup_trigger(
2917 self.render_icon(
2918 "menu", _("Add this graph collection to..."), cssclass="iconbutton inline"),
2919 'add_visual',
2920 'add_visual',
2921 data=["graph_collection", {}, {
2922 'name': self.request.var('name')
2924 url_vars=[("add_type", "graph_collection")])
2926 for img, tooltip in self.status_icons.items():
2927 if isinstance(tooltip, tuple):
2928 tooltip, url = tooltip
2929 self.icon_button(url, tooltip, img, cssclass="inline")
2930 else:
2931 self.icon(tooltip, img, cssclass="inline")
2933 if self.times:
2934 self.measure_time('body')
2935 self.open_div(class_=["execution_times"])
2936 entries = self.times.items()
2937 entries.sort()
2938 for name, duration in entries:
2939 self.div("%s: %.1fms" % (name, duration * 1000))
2940 self.close_div()
2943 # Per request caching
2944 # TODO: Remove this cache here. Do we need some generic functionality
2945 # like this? Probably use some more generic / standard thing.
2948 def set_cache(self, name, value):
2949 self.caches[name] = value
2950 return value
2952 def set_cache_default(self, name, value):
2953 if self.is_cached(name):
2954 return self.get_cached(name)
2955 return self.set_cache(name, value)
2957 def is_cached(self, name):
2958 return name in self.caches
2960 def get_cached(self, name):
2961 return self.caches.get(name)
2963 def del_cache(self, name):
2964 if name in self.caches:
2965 del self.caches[name]
2968 # FIXME: Legacy functions
2971 # TODO: Remove this specific legacy function. Change code using this to valuespecs
2972 def datetime_input(self, varname, default_value, submit=None):
2973 try:
2974 t = self.get_datetime_input(varname)
2975 except:
2976 t = default_value
2978 if varname in self.user_errors:
2979 self.add_user_error(varname + "_date", self.user_errors[varname])
2980 self.add_user_error(varname + "_time", self.user_errors[varname])
2981 self.set_focus(varname + "_date")
2983 br = time.localtime(t)
2984 self.date_input(varname + "_date", br.tm_year, br.tm_mon, br.tm_mday, submit=submit)
2985 self.write_text(" ")
2986 self.time_input(varname + "_time", br.tm_hour, br.tm_min, submit=submit)
2987 self.form_vars.append(varname + "_date")
2988 self.form_vars.append(varname + "_time")
2990 # TODO: Remove this specific legacy function. Change code using this to valuespecs
2991 def time_input(self, varname, hours, mins, submit=None):
2992 self.text_input(
2993 varname,
2994 "%02d:%02d" % (hours, mins),
2995 cssclass="time",
2996 size=5,
2997 submit=submit,
2998 omit_css_width=True)
3000 # TODO: Remove this specific legacy function. Change code using this to valuespecs
3001 def date_input(self, varname, year, month, day, submit=None):
3002 self.text_input(
3003 varname,
3004 "%04d-%02d-%02d" % (year, month, day),
3005 cssclass="date",
3006 size=10,
3007 submit=submit,
3008 omit_css_width=True)
3010 # TODO: Remove this specific legacy function. Change code using this to valuespecs
3011 def get_datetime_input(self, varname):
3012 t = self.request.var(varname + "_time")
3013 d = self.request.var(varname + "_date")
3014 if not t or not d:
3015 raise MKUserError([varname + "_date", varname + "_time"],
3016 _("Please specify a date and time."))
3018 try:
3019 br = time.strptime(d + " " + t, "%Y-%m-%d %H:%M")
3020 except:
3021 raise MKUserError([varname + "_date", varname + "_time"],
3022 _("Please enter the date/time in the format YYYY-MM-DD HH:MM."))
3023 return int(time.mktime(br))
3025 # TODO: Remove this specific legacy function. Change code using this to valuespecs
3026 def get_time_input(self, varname, what):
3027 t = self.request.var(varname)
3028 if not t:
3029 raise MKUserError(varname, _("Please specify %s.") % what)
3031 try:
3032 h, m = t.split(":")
3033 m = int(m)
3034 h = int(h)
3035 if m < 0 or m > 59 or h < 0:
3036 raise Exception()
3037 except:
3038 raise MKUserError(varname, _("Please enter the time in the format HH:MM."))
3039 return m * 60 + h * 3600