2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
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.
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()
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.
69 from contextlib
import contextmanager
75 # suppress missing import error from mypy
76 from html
import escape
as html_escape
# type: ignore
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
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-------------------------------------------------------------.
106 # | | ____|___ ___ __ _ _ __ ___ _ __ |
107 # | | _| / __|/ __/ _` | '_ \ / _ \ '__| |
108 # | | |___\__ \ (_| (_| | |_) | __/ | |
109 # | |_____|___/\___\__,_| .__/ \___|_| |
111 # +----------------------------------------------------------------------+
113 # '----------------------------------------------------------------------
116 class Escaper(object):
118 super(Escaper
, self
).__init
__()
119 self
._unescaper
_text
= re
.compile(
120 r
'<(/?)(h1|h2|b|tt|i|u|br(?: /)?|nobr(?: /)?|pre|a|sup|p|li|ul|ol)>')
121 self
._unescaper
_href
= re
.compile(r
'<a href=(?:"|\')(.*?
)(?
:"
;|
\')>
;')
122 self._unescaper_href_target = re.compile(
123 r'<
;a href
=(?
:"
;|
\')(.*?
)(?
:"
;|
\') target
=(?
:"
;|
\')(.*?
)(?
:"
;|
\')>
;'
126 # Encode HTML attributes. Replace HTML syntax with HTML text.
127 # For example: replace '"' with '"', '<' with '<'.
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)
134 elif attr_type == int:
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("&
;", "&")\
145 .replace(""
;", "\"")\
146 .replace("<
;", "<")\
147 .replace(">
;", ">")
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'&nbsp;', ' ', text)
170 # .--Encoding------------------------------------------------------------.
172 # | | ____|_ __ ___ ___ __| (_)_ __ __ _ |
173 # | | _| | '_ \ / __/ _ \ / _` | | '_ \ / _` | |
174 # | | |___| | | | (_| (_) | (_| | | | | | (_| | |
175 # | |_____|_| |_|\___\___/ \__,_|_|_| |_|\__, | |
177 # +----------------------------------------------------------------------+
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
190 assert isinstance(vars_, list)
192 for varname, value in sorted(vars_):
193 assert isinstance(varname, str)
195 if isinstance(value, int):
197 elif isinstance(value, unicode):
198 value = value.encode("utf
-8")
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.
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
219 if isinstance(value, unicode):
220 value = value.encode("utf
-8")
224 assert isinstance(value, str)
226 return urllib.quote_plus(value)
230 # .--HTML----------------------------------------------------------------.
231 # | _ _ _____ __ __ _ |
232 # | | | | |_ _| \/ | | |
233 # | | |_| | | | | |\/| | | |
234 # | | _ | | | | | | | |___ |
235 # | |_| |_| |_| |_| |_|_____| |
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. |
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 |
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 # '----------------------------------------------------------------------'
253 def __init__(self, value=u''):
254 super(HTML, self).__init__()
255 self.value = self._ensure_unicode(value)
257 def __unicode__(self):
260 def _ensure_unicode(self, thing, encoding_index=0):
262 return unicode(thing)
263 except UnicodeDecodeError:
264 return thing.decode("utf
-8")
266 def __bytebatzen__(self):
267 return self.value.encode("utf
-8")
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.
289 return ("HTML(\"%s\")" % self.value).encode("utf
-8")
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)
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))
340 return HTML(self.value.lower())
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 # | \___/ \__,_|\__| .__/ \__,_|\__|_| \__,_|_| |_|_| |_|\___|_| |
357 # +----------------------------------------------------------------------+
358 # | Provides the write functionality. The method _lowlevel_write needs |
359 # | to be overwritten in the specific subclass! |
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
373 super(OutputFunnel, self).__init__()
376 # Accepts str and unicode objects only!
377 # The plugged functionality can be used for debugging.
378 def write(self, text):
382 if isinstance(text, HTML):
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)
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)
400 def _lowlevel_write(self, text):
401 raise NotImplementedError()
405 self.plug_text.append([])
413 def _is_plugged(self):
414 return bool(self.plug_text)
416 # Get the sink content in order to do something with it.
418 if not self._is_plugged(): # TODO: Raise exception or even remove "if"?
421 text = "".join(self.plug_text.pop())
422 self.plug_text.append([])
427 # .--HTML Generator------------------------------------------------------.
428 # | _ _ _____ __ __ _ |
429 # | | | | |_ _| \/ | | |
430 # | | |_| | | | | |\/| | | |
431 # | | _ | | | | | | | |___ |
432 # | |_| |_| |_| |_| |_|_____| |
435 # | / ___| ___ _ __ ___ _ __ __ _| |_ ___ _ __ |
436 # | | | _ / _ \ '_ \ / _ \ '__/ _` | __/ _ \| '__| |
437 # | | |_| | __/ | | | __/ | | (_| | || (_) | | |
438 # | \____|\___|_| |_|\___|_| \__,_|\__\___/|_| |
440 # +----------------------------------------------------------------------+
441 # | Generator which provides top level HTML writing functionality. |
442 # '----------------------------------------------------------------------'
445 class HTMLGenerator(OutputFunnel):
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 '&
;', '<
;', '>
;' and '"
;'. """
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)
495 super(HTMLGenerator, self).__init__()
496 self.escaper = Escaper()
502 def _render_attributes(self, **attrs):
503 # make class attribute foolproof
505 for k in ["class_", "css", "cssclass", "class"]:
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))
514 # options such as 'selected
' and 'checked
' dont have a value in html tags
517 # render all attributes
518 for k, v in attrs.iteritems():
523 k = self.escaper.escape_attribute(k.rstrip('_
'))
529 if not isinstance(v, list):
530 v = self.escaper.escape_attribute(v)
534 elif k == "style" or k.startswith('on
'):
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)
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]:
567 tag = tag.rstrip("\n")
568 if isinstance(tag_content, HTML):
569 tag += tag_content.lstrip(' ').rstrip('\n')
571 tag += self.escaper.escape_text(tag_content)
573 tag += "</%s>" % (tag_name)
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))
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"]).
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. """
626 def comment(self, comment_text):
627 self.write("<!--%s-->" % self.encode_attribute(comment_text))
629 def meta(self, httpequiv=None, **attrs):
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):
639 self.write_html(self._render_opening_tag('a
', **attrs))
641 def render_a(self, content, href, **attrs):
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):
650 self._render_opening_tag(
651 'link
', rel="stylesheet", type_="text/css", href=href, close_tag=True))
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):
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))
685 def render_label(self, content, for_, **attrs):
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_
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))
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))
738 def render_nbsp(self):
739 return HTML(" ")
742 self.write_html(self.render_nbsp())
746 # .--TimeoutMgr.---------------------------------------------------------.
747 # | _____ _ _ __ __ |
748 # | |_ _(_)_ __ ___ ___ ___ _ _| |_| \/ | __ _ _ __ |
749 # | | | | | '_ ` _ \
/ _ \
/ _ \| | | | __| |\
/| |
/ _` |
'__| |
750 # | | | | | | | | | | __/ (_) | |_| | |_| | | | (_| | | _ |
751 # | |_| |_|_| |_| |_|\___|\___/ \__,_|\__|_| |_|\__, |_|(_) |
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
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
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):
792 # .--Transactions--------------------------------------------------------.
794 # | |_ _| __ __ _ _ __ ___ __ _ ___| |_(_) ___ _ __ ___ |
795 # | | || '__
/ _` |
'_ \/ __|/ _` |/ __| __| |/ _ \| '_ \
/ __| |
796 # | | || | | (_| | | | \__ \ (_| | (__| |_| | (_) | | | \__ \ |
797 # | |_||_| \__,_|_| |_|___/\__,_|\___|\__|_|\___/|_| |_|___/ |
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
815 """Makes the GUI skip all transaction validation steps"""
816 self
._ignore
_transids
= True
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
)
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
:
846 valid_ids
= self
._load
_transids
(lock
=True)
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
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"):
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
:
876 # Normal user/password auth user handling
877 timestamp
= transid
.split("/", 1)[0]
879 # If age is too old (one week), it is always
882 if now
- int(timestamp
) >= 604800: # 7 * 24 hours
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
)
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)
916 valid_ids
.remove(used_id
)
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
):
926 config
.user
.save_file("transids", used_ids
)
930 # .--html----------------------------------------------------------------.
932 # | | |__ | |_ _ __ ___ | | |
933 # | | '_ \| __| '_ ` _ \| | |
934 # | | | | | |_| | | | | | | |
935 # | |_| |_|\__|_| |_| |_|_| |
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")
949 self
._header
_sent
= False
950 self
._context
_buttons
_open
= False
953 self
._body
_classes
= ['main']
954 self
._default
_stylesheets
= ["main_min", "check_mk", "graphs"]
955 self
._default
_javascripts
= ["main"]
958 self
.render_headfoot
= True
959 self
.enable_debug
= False
960 self
.screenshotmode
= False
961 self
.have_help
= False
962 self
.help_visible
= False
965 self
.output_format
= "html"
966 self
.browser_reload
= 0
967 self
.browser_redirect
= ''
968 self
.link_target
= 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
= ""
979 self
.treestates
= None
980 self
.page_context
= {}
987 self
.form_name
= None
992 self
.start_time
= time
.time()
993 self
.last_measurement
= self
.start_time
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"
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"
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
()
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
):
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 "
1050 def _init_debug_mode(self
):
1051 # Debug flag may be set via URL to override the configuration
1052 if self
.request
.var("debug"):
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
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]
1078 # Redirect to mobile GUI if we are a mobile device and the index is requested
1079 if myfile
== "index" and self
.mobile
:
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"
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/
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",
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\\-",
1107 return reg_b
.search(user_agent
) or reg_v
.search(user_agent
[0:4])
1110 # HTTP variable processing
1114 def stashed_vars(self
):
1115 saved_vars
= dict(self
.request
.itervars())
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."""
1127 value
= self
.request
.var(varname
, deflt
)
1128 if value
is not None:
1129 value
.decode("ascii")
1131 except UnicodeDecodeError:
1132 raise MKUserError(varname
, _("The given text must only contain ASCII characters."))
1134 def get_unicode_input(self
, varname
, deflt
=None):
1136 val
= self
.request
.var(varname
, deflt
)
1137 return val
.decode("utf-8") if isinstance(val
, str) else val
1138 except UnicodeDecodeError:
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
):
1149 return int(self
.request
.var(varname
))
1151 raise MKUserError(varname
, _("The parameter \"%s\" is missing.") % varname
)
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:
1171 raise MKUserError(varname
, _("The parameter \"%s\" is missing.") % varname
)
1173 url
= self
.request
.var(varname
)
1174 if not utils
.is_allowed_url(url
):
1177 raise MKUserError(varname
, _("The parameter \"%s\" is not a valid URL.") % varname
)
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:
1189 if self
.request
.var("request_format") == "python":
1191 python_request
= self
.request
.var("request", "{}")
1192 request
= ast
.literal_eval(python_request
)
1193 except (SyntaxError, ValueError) as e
:
1196 _("Failed to parse Python request: '%s': %s") % (python_request
, e
))
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")
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()
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 '"', '<' and '>' with '<' and '>'
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
)
1257 # remove all HTML-tags
1258 def strip_tags(self
, ht
):
1260 if isinstance(ht
, HTML
):
1263 if not isinstance(ht
, six
.string_types
):
1273 ht
= ht
[0:x
] + ht
[y
+ 1:]
1274 return ht
.replace(" ", " ")
1276 def strip_scripts(self
, ht
):
1278 x
= ht
.find('<script')
1281 y
= ht
.find('</script>')
1284 ht
= ht
[0:x
] + ht
[y
+ 9:]
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()
1301 def set_output_format(self
, f
):
1303 content_type
= "application/json; charset=UTF-8"
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"
1312 content_type
= "text/plain; charset=UTF-8"
1315 content_type
= "text/plain; charset=UTF-8"
1318 content_type
= "text/html; charset=UTF-8"
1321 content_type
= "text/xml; charset=UTF-8"
1324 content_type
= "application/pdf"
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"
1339 def measure_time(self
, name
):
1340 self
.times
.setdefault(name
, 0.0)
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
):
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):
1398 self
.status_icons
[img
] = tooltip
, url
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()")
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", {})
1440 """Finish the HTTP request processing before handing over to the application server"""
1441 self
.transaction_manager
.store_new()
1442 self
.disable_request_timeout()
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':
1473 prefix
= _('MESSAGE')
1474 elif what
== 'warning':
1476 prefix
= _('WARNING')
1483 if self
.output_format
== "html":
1484 code
+= self
.render_div(self
.render_text(msg
), class_
=cls
)
1486 code
+= self
.render_center(code
)
1488 code
+= self
.render_text('%s: %s\n' % (prefix
, self
.strip_tags(msg
)))
1492 def show_localization_hint(self
):
1493 url
= "wato.py?mode=edit_configvar&varname=user_localizations"
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
)
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
)
1524 # Debugging, diagnose and logging
1527 def debug(self
, *x
):
1530 formatted
= pprint
.pformat(element
)
1531 except UnicodeDecodeError:
1532 formatted
= repr(element
)
1533 self
.write(self
.render_pre(formatted
))
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
]
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"
1551 return filename
+ "?" + self
.urlencode_vars(vars_
)
1554 def makeuri_contextless(self
, vars_
, filename
=None):
1556 filename
= self
.myfile
+ ".py"
1558 return filename
+ "?" + self
.urlencode_vars(vars_
)
1561 def makeactionuri(self
, addvars
, filename
=None, delvars
=None):
1562 return self
.makeuri(
1563 addvars
+ [("_transid", self
.transaction_manager
.get())],
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")
1579 self
._render
_opening
_tag
(
1581 rel
="shortcut icon",
1582 href
=self
._detect
_themed
_image
_path
("images/favicon.ico"),
1586 def _head(self
, title
, javascripts
=None, stylesheets
=None):
1588 javascripts
= javascripts
if javascripts
else []
1589 stylesheets
= stylesheets
if stylesheets
else ["pages"]
1593 self
.default_html_headers()
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
()
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
))
1628 self
.javascript('cmk.utils.set_reload(%s)' % (self
.browser_reload
))
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
:
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([])
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"]
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
__)
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')
1696 self
._head
(title
, javascripts
, stylesheets
)
1697 self
._header
_sent
= True
1704 show_body_start
=True,
1705 show_top_heading
=True):
1706 if self
.output_format
== "html":
1707 if not self
._header
_sent
:
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
):
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()
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 ' %
1742 if config
.pagetitle_date_format
:
1743 self
.write(' <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")
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
))
1758 onfocus
="if (this.blur) this.blur();",
1759 onclick
="this.innerHTML=\'%s\'; document.location.reload();" % _("Reloading..."))
1762 def top_heading_right(self
):
1763 cssclass
= "active" if self
.help_visible
else "passive"
1767 _("Toggle context help texts"),
1770 onclick
="cmk.help.toggle()",
1771 style
="display:none",
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"))
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":
1787 self
.bottom_footer()
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")
1799 self
.open_td(class_
="left")
1800 self
._write
_status
_icons
()
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
)
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
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" \
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")
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();")
1847 # HTML form rendering
1850 def begin_form(self
, name
, action
=None, method
="GET", onsubmit
=None, add_transid
=True):
1853 action
= self
.myfile
+ ".py"
1854 self
.current_form
= name
1856 id_
="form_%s" % name
,
1862 enctype
="multipart/form-data" if method
.lower() == "post" else None)
1863 self
.hidden_field("filled_in", name
, add_var
=True)
1865 self
.hidden_field("_transid", str(self
.transaction_manager
.get()))
1866 self
.form_name
= name
1870 self
.form_name
= None
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:
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):
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):
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
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(
1953 class_
=["button", cssclass
if cssclass
else None],
1958 def buttonlink(self
,
1968 href
+= "&_transid=%s" % self
.transaction_manager
.get()
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"]
1976 if not isinstance(class_
, list):
1977 css_classes
.append(class_
)
1979 css_classes
.extend(class_
)
1990 onclick
="location.href=\'%s\'" % href
)
1992 # TODO: Refactor the arguments. It is only used in views/wato
1993 def toggle_button(self
,
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_
2009 state
= "off" if disabled
else "on"
2020 id_
="%s_%s" % (id_
, state
),
2021 class_
=["togglebutton", state
, icon
, cssclass
],
2023 style
='display:none' if hidden
else None,
2025 self
.open_a("javascript:void(0)", onclick
=onclick
)
2026 self
.icon(title
=None, icon
=icon
)
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.
2049 # Same API as other elements: class_ can be a list or string/None
2052 classes
= class_
if isinstance(class_
, list) else [class_
]
2055 classes
.append("disabled")
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.
2066 class_
=["button", cssclass
] + classes
,
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
)
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
2090 message
= msg_or_exc
2092 if isinstance(varname
, list):
2094 self
.add_user_error(v
, message
)
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()))
2107 def text_input(self
,
2120 error
= self
.user_errors
.get(varname
)
2121 value
= self
.request
.var(varname
, default_value
)
2125 self
.set_focus(varname
)
2126 self
.form_vars
.append(varname
)
2129 style
, size
= None, None
2130 if args
.get("try_max_width"):
2131 style
= "width: calc(100% - 10px); "
2133 cols
= int(args
["size"])
2136 style
+= "min-width: %d.8ex; " % cols
2138 elif "size" in args
and args
["size"]:
2139 if args
["size"] == "max":
2140 style
= "width: 100%;"
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
))
2161 "autocomplete": args
.get("autocomplete"),
2162 "readonly": "true" if args
.get("read_only") else None,
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
2174 self
.open_x(class_
="inputerror")
2177 self
.label(label
, for_
=id_
)
2178 self
.write_html(self
.render_input(varname
, type_
=args
.get("type_", "text"), **attributes
))
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
2193 class_
=["status_label", button_cls
, status
],
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_"]]
2203 attrs
["class_"] = []
2205 attrs
["class_"] += [
2207 "on" if enabled
else "off",
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
)
2219 def number_input(self
, varname
, deflt
="", size
=8, style
="", submit
=None):
2220 if deflt
is not None:
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):
2232 value
= self
.request
.var(varname
, deflt
)
2233 error
= self
.user_errors
.get(varname
)
2235 self
.form_vars
.append(varname
)
2237 self
.set_focus(varname
)
2240 style
= "width: %d.8ex;" % cols
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)
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).
2258 if value
and value
.startswith("\n"):
2259 value
= "\n" + value
2262 self
.open_x(class_
="inputerror")
2263 self
.write_html(self
._render
_content
_tag
("textarea", value
, **attrs
))
2267 # TODO: DEPRECATED!!
2268 def sorted_select(self
, varname
, choices
, deflt
='', onchange
=None, attrs
=None):
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):
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
)
2290 self
.form_vars
.append(varname
)
2291 attrs
.setdefault('size', 1)
2295 # Sort according to display texts, not keys
2296 chs
.sort(key
=lambda a
: a
[1].lower())
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")
2315 attrs
["class_"] = ["select2-enable", attrs
["class_"]]
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)
2328 def icon_dropdown(self
, varname
, choices
, deflt
=""):
2329 current
= self
.request
.var(varname
, deflt
)
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
)
2339 value
=value
if value
else "",
2340 selected
='' if selected
else None,
2341 style
="background-image:url(images/icon_%s.png);" % icon
)
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
)
2352 self
.open_x(class_
="inputerror")
2353 self
.input(name
=varname
, type_
="file")
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
)
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"), "")
2389 return False # False --> "Dialog shown, no answer yet"
2391 # Now check the transaction
2392 return True if self
.check_transaction(
2393 ) else None # True: "Yes", None --> Browser reload of "yes" page
2399 def begin_radio_group(self
, horizontal
=False):
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
):
2406 self
.write(self
._render
_closing
_tag
("fieldset"))
2408 def radiobutton(self
, varname
, value
, checked
, label
):
2410 self
.form_vars
.append(varname
)
2413 if self
.request
.has_var(varname
):
2414 checked
= self
.request
.var(varname
) == value
2417 id_
= "rb_%s_%s" % (varname
, value
) if label
else None
2418 self
.open_span(class_
="radiobutton_group")
2420 name
=varname
, type_
="radio", value
=value
, checked
='' if checked
else None, id_
=id_
)
2422 self
.label(label
, for_
=id_
)
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
2449 error
= self
.user_errors
.get(varname
)
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")
2461 code
= self
.render_x(code
, class_
="inputerror")
2463 self
.form_vars
.append(varname
)
2470 def begin_foldable_container(self
,
2481 self
.folding_indent
= indent
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")
2495 self
.img(class_
=["treeangle", "title"], src
="images/icon_%s.png" % icon
)
2499 class_
=["treeangle", "nform", "open" if isopen
else "closed"],
2500 src
="images/%s_closed.png" % tree_img
,
2502 self
.write_text(title
)
2506 self
.open_div(class_
="foldable")
2510 id_
="treeimg.%s.%s" % (treename
, id_
),
2511 class_
=["treeangle", "open" if isopen
else "closed"],
2512 src
="images/%s_closed.png" % tree_img
,
2515 if isinstance(title
, HTML
): # custom HTML code
2518 class_
=["treeangle", "title"],
2519 src
="images/icon_%s.png" % icon
,
2521 self
.write_text(title
)
2522 if indent
!= "form":
2525 self
.open_b(class_
=["treeangle", "title"], onclick
=None if title_url
else onclick
)
2527 self
.img(class_
=["treeangle", "title"], src
="images/icon_%s.png" % icon
)
2529 self
.a(title
, href
=title_url
)
2531 self
.write_text(title
)
2535 indent_style
= "padding-left: %dpx; " % (indent
is True and 15 or 0)
2536 if indent
== "form":
2540 indent_style
+= "margin: 0; "
2542 id_
="tree.%s.%s" % (treename
, id_
),
2543 class_
=["treeangle", "open" if isopen
else "closed"],
2546 # give caller information about current toggling state (needed for nform)
2549 def end_foldable_container(self
):
2550 if self
.folding_indent
!= "nform":
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"
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
:
2576 title
=_("Show all buttons"), id="toggle", class_
=["contextlink", "short"])
2577 self
.a("...", onclick
='cmk.utils.unhide_context_buttons(this);', href
='#')
2579 self
.div("", class_
="end")
2581 self
._context
_buttons
_open
= False
2583 def context_button(self
,
2592 self
._context
_button
(
2599 hover_title
=hover_title
,
2602 def _context_button(self
,
2611 title
= self
.attrencode(title
)
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
2620 self
.context_button_hidden
= True
2622 if not self
._context
_buttons
_open
:
2623 self
.begin_context_buttons()
2625 css_classes
= ["contextlink"]
2627 css_classes
.append("hot")
2629 if isinstance(class_
, list):
2630 css_classes
+= class_
2632 css_classes
+= class_
.split(" ")
2634 self
.open_div(class_
=css_classes
, id_
=id_
, style
="display:%s;" % display
)
2639 onclick
="cmk.utils.count_context_button(this);" if bestof
else None)
2642 self
.icon('', icon
, cssclass
="inline", middle
=False)
2654 def begin_floating_options(self
, div_id
, is_open
):
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")
2661 def end_floating_options(self
, reset_url
=None):
2666 self
.button("apply", _("Apply"), "submit")
2668 self
.buttonlink(reset_url
, _("Reset to defaults"))
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
)
2684 # HTML icon rendering
2687 # FIXME: Change order of input arguments in one: icon and render_icon!!
2688 def icon(self
, title
, icon
, **kwargs
):
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):
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
)
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
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
,
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
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
)
2758 def render_icon_button(self
,
2768 # Same API as other elements: class_ can be a list or string/None
2771 classes
.append(cssclass
)
2773 classes
= class_
if isinstance(class_
, list) else [class_
]
2775 icon
= HTML(self
.render_icon(icon
, cssclass
="iconbutton"))
2777 return self
.render_a(
2783 'target': target
if target
else '',
2784 'href': url
if not onclick
else "javascript:void(0)",
2785 'onfocus': "if (this.blur) this.blur();",
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
,
2807 onclick
= 'cmk.popup_menu.toggle_popup(event, this, %s, %s, %s, %s, %s, %s, %s);' % \
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
):
2825 self
.render_element_dragger(
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
):
2833 self
.render_element_dragger(
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
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"]:
2866 if not prefix
or name
.startswith(prefix
):
2867 self
.tr(self
.render_td(name
, class_
="left") + self
.render_td(value
, class_
="right"))
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
):
2874 self
.makeuri([]), _("URL to this frame"), "frameurl", target
="_top", cssclass
="inline")
2876 "index.py?" + self
.urlencode_vars([("start_url", self
.makeuri([]))]),
2877 _("URL to this page including sidebar"),
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"):
2886 self
.makeuri([("output_format", "csv_export")]),
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"
2897 for k
, v
in self
.page_context
.items():
2900 elif isinstance(v
, unicode):
2901 v
= v
.encode('utf-8')
2905 self
.render_icon("menu", _("Add this view to..."), cssclass
="iconbutton inline"),
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":
2918 "menu", _("Add this graph collection to..."), cssclass
="iconbutton inline"),
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")
2931 self
.icon(tooltip
, img
, cssclass
="inline")
2934 self
.measure_time('body')
2935 self
.open_div(class_
=["execution_times"])
2936 entries
= self
.times
.items()
2938 for name
, duration
in entries
:
2939 self
.div("%s: %.1fms" % (name
, duration
* 1000))
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
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):
2974 t
= self
.get_datetime_input(varname
)
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):
2994 "%02d:%02d" % (hours
, mins
),
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):
3004 "%04d-%02d-%02d" % (year
, month
, day
),
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")
3015 raise MKUserError([varname
+ "_date", varname
+ "_time"],
3016 _("Please specify a date and time."))
3019 br
= time
.strptime(d
+ " " + t
, "%Y-%m-%d %H:%M")
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
)
3029 raise MKUserError(varname
, _("Please specify %s.") % what
)
3035 if m
< 0 or m
> 59 or h
< 0:
3038 raise MKUserError(varname
, _("Please enter the time in the format HH:MM."))
3039 return m
* 60 + h
* 3600