cryptonas/src/cryptobox/web/sites.py
lars 794998f950 broken interface fixed in 'partition' plugin for ie
rendering bug of volume_properties fixed for ie
fixed screen width in a mozilla/ie compatible way
added german translation: 'log', 'network', 'volume_automount' and 'volume_details'
fixed config management of 'plugin_manager' plugin
fixed filtering of log level messages for 'logs' plugin
updated documentation for ssl configurations
changed default installation destinations in setup.py
added nice background images to environment and help messages
replaced message 'div' with 'fieldset'
moved stylesheet data of plugins to html header (as required by spec)
removed obsolete css definitions
removed obsolete old perl/bash code
improved 'update_po_files': remove obsolete msgids
functionality of 'update_english.sh' moved to 'update_po_files'
omit 'weblang' link attribute if it does not change the default setting
changed default language from 'de' to 'en'
fixed template bug that prevented the translation of plugin links
fixed invalid html
implement filecheck overriding for unittests
2006-12-18 13:37:08 +00:00

649 lines
23 KiB
Python

#
# Copyright 2006 sense.lab e.V.
#
# This file is part of the CryptoBox.
#
# The CryptoBox is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# The CryptoBox is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with the CryptoBox; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
""" this module handles all http requests and renders a website """
__revision__ = "$Id$"
import cryptobox.core.main
import cryptobox.web.dataset
import cryptobox.plugins.manage
import cryptobox.core.exceptions
import re
import cherrypy
import os
import sys
try:
import neo_cgi, neo_util, neo_cs
except ImportError:
_ERRMSG = "Could not import clearsilver module. \
Try 'apt-get install python-clearsilver'."
sys.stderr.write(_ERRMSG)
raise ImportError, _ERRMSG
GETTEXT_DOMAIN = 'cryptobox-server'
class PluginIconHandler:
"""deliver the icons of available plugins via cherrypy"""
def __init__(self, plugins):
for plugin in plugins.get_plugins():
if not plugin:
continue
plname = plugin.get_name()
## expose the get_icon function of this plugin
setattr(self, plname, plugin.get_icon)
class WebInterfaceSites:
"""handle all http requests and render pages
this includes:
- filtering common arguments
- calling feature actions
- translating content
all available features are dynamically exposed
"""
## this template is used under strange circumstances
defaultTemplate = "empty"
def __init__(self, conf_file=None):
## we should only use variables preceded by "__" to avoid name conflicts
## when loading features
self.cbox = cryptobox.core.main.CryptoBox(conf_file)
self.__cached_language_data = None
self.__dataset = None
self.icons = None
self.__plugin_manager = None
self.__reset_dataset()
## store the original http error handler
self._cp_on_http_error = self.new_http_error_handler
## set initial language order
self.lang_order = self.cbox.prefs["WebSettings"]["Languages"][:]
self.setup()
def setup(self):
"""Prepare the webinterface.
"""
self.cbox.setup()
for plugin in self.__plugin_manager.get_plugins():
if plugin:
plugin.setup()
def cleanup(self):
"""Shutdown the webinterface safely.
"""
for plugin in self.__plugin_manager.get_plugins():
if plugin:
plugin.cleanup()
self.cbox.cleanup()
def __reset_dataset(self):
"""this method has to be called at the beginning of every "site" action
important: only at the beginning of an action (to not loose information)
important: for _every_ "site" action (cherrypy is stateful)
also take care for the plugins, as they also contain datasets
"""
self.__load_plugins()
self.__dataset = cryptobox.web.dataset.WebInterfaceDataset(
self.cbox, self.cbox.prefs, self.__plugin_manager.get_plugins())
## publish plugin icons
self.icons = PluginIconHandler(self.__plugin_manager)
self.icons.exposed = True
## check, if a configuration partition has become available
self.cbox.prefs.prepare_partition()
def __load_plugins(self):
"""reinitialize the list of available plugins
this includes the following:
- reload all plugins and check their state (disabled or not)
- reinitilize the datasets of all plugins
"""
self.__plugin_manager = cryptobox.plugins.manage.PluginManager(
self.cbox, self.cbox.prefs["Locations"]["PluginDir"], self)
for plugin in self.__plugin_manager.get_plugins():
if not plugin:
continue
plname = plugin.get_name()
## check if there are name conflicts: e.g. a local variable has the
## same name as a plugin to be loaded -> skip these plugins
## if we do not check this here, nasty side effects may occour ...
try:
prev_obj = getattr(self, plname)
if not callable(prev_obj) \
or not prev_obj.exposed:
self.cbox.log.error("Skipped feature (%s) as its name"
+ " conflicts with a local variable - see"
+ " module cryptobox.web.sites" % plname)
## skip this plugin
continue
except (NameError, AttributeError):
## an attribute with the same name does not exist -> ok
if plugin.is_enabled():
self.cbox.log.info("Plugin '%s' loaded" % plname)
## expose all features as URLs
setattr(self, plname, self.return_plugin_action(plugin))
getattr(self, plname).exposed = True
#TODO: check, if this really works
#for now the "stream_response" feature seems to be broken
#setattr(getattr(self, plname), "stream_respones", True)
else:
self.cbox.log.info("Plugin '%s' is disabled" % plname)
## remove the plugin, if it was active before
setattr(self, plname, None)
## sub pages requiring authentication may not be defined above
def __request_auth(self=None):
""" this is a function decorator to check authentication
"""
def check_credentials(site):
""" see description of _inner_wrapper - please simplify this!
"""
def _inner_wrapper(self, *args, **kargs):
"""this function was necessary while trying around with the
function decorator - if someone can implement the decorator
with less effort, then any suggestions are welcome!
"""
import base64
## define a "non-allowed" function
user, password = None, None
try:
## ignore the "Basic " (first six letters) part
resp = cherrypy.request.headers["Authorization"][6:]
(user, password) = base64.b64decode(resp).split(":", 1)
except KeyError:
## no "authorization" header was sent
pass
except TypeError:
## invalid base64 string
pass
except AttributeError:
## no cherrypy response header defined
pass
auth_dict = self.cbox.prefs.user_db["admins"]
if user in auth_dict.keys():
if self.cbox.prefs.user_db.get_digest(password) == auth_dict[user]:
## ok: return the choosen page
self.cbox.log.info("access granted for: %s" % user)
return site(self, *args, **kargs)
else:
self.cbox.log.info(
"wrong password supplied for: %s" % user)
else:
self.cbox.log.info("unknown user: %s" % str(user))
## wrong credentials: return "access denied"
cherrypy.response.headers["WWW-Authenticate"] = \
'''Basic realm="CryptoBox"'''
cherrypy.response.status = 401
return self.__render("access_denied")
return _inner_wrapper
return check_credentials
######################################################################
## put real sites down here and don't forget to expose them at the end
@cherrypy.expose
def index(self, weblang="", help="0", device=None):
"""the default page on startup - we show the list of available disks
"""
self.__reset_dataset()
self.__set_web_lang(weblang)
self.__check_environment()
## do not forget the language!
param_dict = {"weblang":weblang}
## render "disks" plugin by default
return self.return_plugin_action(
self.__plugin_manager.get_plugin("disks"))(**param_dict)
def new_http_error_handler(self, error_code, message):
"""handle http errors gracefully
404 - not found errors: ignored if url is below /cryptobox-misc/
other 404 errors: send the error code and return a nice informative page
500 - runtime errors: return "ok" exit code and show a polite excuse
others: are there any other possible http errors?
"""
import traceback
## we ignore uninteresting not-found errors
if (error_code == 404) and \
(cherrypy.request.path.startswith("/cryptobox-misc/") or \
cherrypy.request.path in ['/robots.txt','/favicon.ico']):
cherrypy.response.status = error_code
return
## an invalid action was requested
if error_code == 404:
## we send a not-found error (with the usual interface)
cherrypy.response.status = error_code
self.__dataset["Data.Warning"] = "InvalidAction"
cherrypy.response.body = self.__render("empty")
return
## are there still bugs in the code?
if error_code == 500:
## we fix the error code (200 is "OK")
cherrypy.response.status = 200
self.cbox.log.error(
"HTTP-ERROR[500] - runtime error: %s" % str(message))
## add a traceback and exception information to the lo
for log_line in traceback.format_exception(*sys.exc_info()):
self.cbox.log.error("\t%s" % log_line)
self.__dataset["Data.Warning"] = "RuntimeError"
cherrypy.response.body = self.__render("empty")
return
## unknown error type
cherrypy.response.status = error_code
self.cbox.log.warn("HTTP-ERROR[%d] - an unknown error occoured: %s" \
% (error_code, message))
cherrypy.response.body = self.__render("empty")
def return_plugin_action(self, plugin):
""" returns a function that is suitable for handling a cherrypy
page request
"""
def handler(self, weblang="", device=None, help="0", redirect=None, message_keep=None, **args):
"""this function handles a cherrypy page request
"""
plugin.reset()
self.__reset_dataset()
self.__check_environment()
self.__set_web_lang(weblang)
## we always read the "device" setting - otherwise volume-plugin
## links would not work easily
## (see "volume_props" linking to "volume_format_fs")
## it will get ignored for non-volume plugins
plugin.device = None
if device and self.__set_device(device):
plugin.device = device
## check the device argument of volume plugins
if "volume" in plugin.plugin_capabilities:
## initialize the dataset of the selected device if necessary
if plugin.device:
self.__dataset.set_current_disk_state(plugin.device)
else:
## invalid (or missing) device setting
return self.__render(self.defaultTemplate)
## check if there is a "redirect" setting - this will override
## the return value of the do_action function
## (e.g. useful for umount-before-format)
override_next_template = None
if redirect:
override_next_template = { "plugin": redirect }
if "volume" in plugin.plugin_capabilities:
override_next_template["values"] = {"device":plugin.device}
## check for information to be kept after the last call
if message_keep:
for (key, value) in message_keep["dataset"].items():
self.__dataset[key] = value
## check if the device is busy
if plugin.device and self.cbox.get_container(plugin.device).is_busy():
return self.__render("volume_busy")
## call the plugin handler
next_template = plugin.do_action(**args)
## for 'volume' plugins: reread the dataset of the current disk
## additionally: set the default template for plugins
if "volume" in plugin.plugin_capabilities:
## maybe the state of the current volume was changed?
self.__dataset.set_current_disk_state(plugin.device)
if not next_template:
next_template = { "plugin":"volume_mount",
"values":{"device":plugin.device}}
else:
## some non-volume plugins change the internal state of other
## plugins - e.g.: plugin_manager
self.__dataset.set_plugin_data()
## default page for non-volume plugins is the disk selection
if not next_template:
next_template = { "plugin":"disks", "values":{} }
## some non-volume plugins may change the state of containers
## the mount plugin may change the number of active disks - for the logo
self.__dataset.set_containers_state()
## was a redirect requested?
if override_next_template:
next_template = override_next_template
## if another plugins was choosen for 'next_template', then do it!
if isinstance(next_template, dict) \
and "plugin" in next_template.keys() \
and "values" in next_template.keys() \
and self.__plugin_manager.get_plugin(next_template["plugin"]):
value_dict = dict(next_template["values"])
## force the current weblang attribute - otherwise it gets lost
value_dict["weblang"] = self.lang_order[0]
## check for warnings/success messages, that should be kept
if "Data.Success" in plugin.hdf.keys() \
or "Data.Warning" in plugin.hdf.keys():
value_dict["message_keep"] = {"plugin":plugin, "dataset":{}}
for keep_key in ("Data.Warning", "Data.Success"):
if keep_key in plugin.hdf.keys():
self.cbox.log.info("keeping message: %s" % \
plugin.hdf[keep_key])
value_dict["message_keep"]["dataset"][keep_key] = \
plugin.hdf[keep_key]
new_plugin = self.__plugin_manager.get_plugin(next_template["plugin"])
return self.return_plugin_action(new_plugin)(**value_dict)
## save the currently active plugin name
self.__dataset["Data.ActivePlugin"] = plugin.get_name()
return self.__render(next_template, plugin)
## apply authentication?
if plugin.is_auth_required():
return lambda **args: self.__request_auth()(handler)(self, **args)
else:
return lambda **args: handler(self, **args)
@cherrypy.expose
def test(self, weblang="", help="0", device=None):
"""test authentication - this function may be safely removed
"""
self.__reset_dataset()
self.__set_web_lang(weblang)
self.__check_environment()
return "test passed"
@cherrypy.expose
def test_stream(self, weblang="", help="0", device=None):
"""just for testing purposes - to check if the "stream_response" feature
actually works - for now (September 02006) it does not seem to be ok
"""
import time
yield "<html><head><title>neu</title></head><body><p><ul>"
for num in range(10):
yield "<li>yes: %d - %s</li>" % (num, str(time.time()))
time.sleep(1)
yield "</ul></p></html>"
##################### input checker ##########################
def __check_environment(self):
"""inform the user of suspicious environmental problems
examples are: non-https, readonly-config, ...
"""
## this check is done _after_ "reset_dataset" -> if there is
## a config partition, then it was loaded before
if self.cbox.prefs.requires_partition() \
and not self.cbox.prefs.get_active_partition():
self.__dataset["Data.EnvironmentWarning"] = "ReadOnlyConfig"
#TODO: turn this on soon (add "not") - for now it is annoying
if not self.__check_https():
self.__dataset["Data.EnvironmentWarning"] = "NoSSL"
def __check_https(self):
"""check the request scheme
"""
if cherrypy.request.scheme == "https":
return True
## check an environment setting - this is quite common behind proxies
if os.environ.has_key("HTTPS"):
return True
## this arbitrarily chosen header must be documented in README.proxy
#TODO: check http://jamesthornton.com/writing/openacs-pound.html for this
if cherrypy.request.headers.has_key("X-SSL-Request") \
and (cherrypy.request.headers["X-SSL-Request"] == "1"):
return True
## plaintext connection
return False
def __set_web_lang(self, value):
"""set the preferred priority of languages according to this order:
1. language selected via web interface
2. preferred browser language setting
3. languages defined in the config file
"""
## start with the configured language order
lang_order = self.cbox.prefs["WebSettings"]["Languages"][:]
self.cbox.log.debug(
"updating language preferences (default: %s)" % str(lang_order))
## put the preferred browser language in front
guess = self.__get_browser_language(lang_order)
if guess:
lang_order.remove(guess)
lang_order.insert(0, guess)
self.cbox.log.debug(
"raised priority of preferred browser language: %s" % guess)
## check if the 'weblang' setting is necessary (does it change the result
## of the language preference calculation?)
override_by_weblang = False
## is the chosen language (via web interface) valid? - put it in front
if value and (value in lang_order) and (not re.search(r'\W', value)):
## skip if the 'weblang' value is already at the top of the list
if lang_order.index(value) != 0:
override_by_weblang = True
lang_order.remove(value)
lang_order.insert(0, value)
self.cbox.log.debug(
"raised priority of selected language: %s" % value)
elif value:
self.cbox.log.info("invalid language selected: %s" % value)
## store current language setting
self.cbox.log.info(
"current language preference: %s" % str(lang_order))
self.lang_order = lang_order
self.__dataset["Settings.Language"] = lang_order[0]
## we do not have to add the LinkAttr if it is irrelevant
if override_by_weblang:
self.__dataset["Settings.LinkAttrs.weblang"] = lang_order[0]
def __get_browser_language(self, avail_langs):
"""guess the preferred language of the user (as sent by the browser)
take the first language, that is part of 'avail_langs'
"""
try:
pref_lang_header = cherrypy.request.headers["Accept-Language"]
except KeyError:
## no language header was specified
return None
## this could be a typical 'Accept-Language' header:
## de-de,de;q=0.8,en-us;q=0.5,en;q=0.3
regex = re.compile(r"\w+(-\w+)?(;q=[\d\.]+)?$")
pref_langs = [e.split(";", 1)[0]
for e in pref_lang_header.split(",")
if regex.match(e)]
## is one of these preferred languages available?
for lang in pref_langs:
if lang in avail_langs:
return lang
## we try to be nice: also look for "de" if "de-de" was specified ...
for lang in pref_langs:
## use only the first part of the language
short_lang = lang.split("-", 1)[0]
if short_lang in avail_langs:
return short_lang
## we give up
return None
def __set_device(self, device):
"""check a device name that was chosen via the web interface
issue a warning if the device is invalid"""
if device and re.match(r'[\w /\-]+$', device) \
and self.cbox.get_container(device):
self.cbox.log.debug("select device: %s" % device)
return True
else:
self.cbox.log.warn("invalid device: %s" % device)
self.__dataset["Data.Warning"] = "InvalidDevice"
return False
def __substitute_gettext(self, languages, text_domain, hdf):
"""substitute all texts in the hdf dataset with their translated
counterparts as returned by gettext
"""
import gettext
try:
translator = gettext.translation(text_domain, languages=languages)
except IOError, err_msg:
## no translation found
self.cbox.log.warn("unable to load language file: %s" % err_msg)
return hdf
def walk_tree(parent_name, hdf_node):
"""iterate through all nodes"""
def translate_node(node):
"""turn one single string into unicode"""
if not node.value():
return
for (key, value) in node.attrs():
## ignore all nodes with the 'LINK' attribute
## for now clearsilver is buggy regarding attributes
## buggy -> parsing of a hdf file fails silently
if key == "LINK":
return
## as long as the attributes do not work, we have to rely on
## some magic names to ignore translations
if (parent_name == "Link") and \
(node.name() in ["Rel", "Prot", "Abs"]):
return
try:
#TODO: we should use unicode - or not? - turn it on later
#node.setValue("", translator.ugettext(node.value()))
## quite obscure: ugettext can handle None - gettext breaks instead
node.setValue("", translator.gettext(node.value()))
except UnicodeEncodeError, err_msg:
self.cbox.log.info(
"Failed unicode encoding for gettext: %s - %s" \
% (node.value(),err_msg))
## fallback to default encoding
node.setValue("", translator.gettext(node.value()))
while hdf_node:
translate_node(hdf_node)
walk_tree(hdf_node.name(), hdf_node.child())
hdf_node = hdf_node.next()
walk_tree("", hdf)
def __get_language_data(self):
"""return the hdf dataset of the main interface and all plugins
translations are done according to self.lang_order
"""
## check if the language setting has changed - use cache if possible
if self.__cached_language_data and \
self.__cached_language_data["lang_order"] == self.lang_order:
self.cbox.log.debug(
"using cached language data: %s" % str(self.lang_order))
return self.__cached_language_data["hdf"]
self.cbox.log.debug("generating language data")
hdf = neo_util.HDF()
hdf.readFile(os.path.join(
self.cbox.prefs["Locations"]["TemplateDir"],"language.hdf"))
self.__substitute_gettext(self.lang_order, GETTEXT_DOMAIN, hdf)
## load the language data of all plugins
for plugin in self.__plugin_manager.get_plugins():
pl_lang = plugin.get_language_data()
self.__substitute_gettext(self.lang_order, "%s-feature-%s" % \
(GETTEXT_DOMAIN, plugin.get_name()), pl_lang)
hdf.copy("Plugins.%s" % plugin.get_name(), pl_lang)
self.cbox.log.debug(
"language data for plugin loaded: %s" % plugin.get_name())
## cache result for later retrieval
self.__cached_language_data = \
{"lang_order": self.lang_order, "hdf": hdf}
return hdf
def __render(self, render_info, plugin=None):
'''renders from clearsilver templates and returns the resulting html
'''
## is render_info a string (filename of the template) or a dictionary?
if isinstance(render_info, dict):
template = render_info["template"]
if render_info.has_key("generator"):
generator = render_info["generator"]
else:
generator = None
else:
(template, generator) = (render_info, None)
## load the language data
hdf = neo_util.HDF()
hdf.copy("Lang", self.__get_language_data())
## first: assume, that the template file is in the global
## template directory
self.__dataset["Settings.TemplateFile"] = os.path.abspath(os.path.join(
self.cbox.prefs["Locations"]["TemplateDir"],
template + ".cs"))
if plugin:
## check, if the plugin provides the template file -> overriding
plugin_cs_file = plugin.get_template_filename(template)
if plugin_cs_file:
self.__dataset["Settings.TemplateFile"] = plugin_cs_file
## add the current state of the plugins to the hdf dataset
self.__dataset["Data.Status.Plugins.%s" % plugin.get_name()] = \
plugin.get_status()
## load the dataset of the plugin
plugin.load_dataset(hdf)
self.cbox.log.info("rendering site: " + template)
cs_path = os.path.abspath(os.path.join(
self.cbox.prefs["Locations"]["TemplateDir"], "main.cs"))
if not os.access(cs_path, os.R_OK):
self.cbox.log.error(
"Couldn't read clearsilver file: %s" % cs_path)
yield "Couldn't read clearsilver file: %s" % cs_path
return
self.cbox.log.debug(self.__dataset)
for key in self.__dataset.keys():
hdf.setValue(key, str(self.__dataset[key]))
cs_data = neo_cs.CS(hdf)
cs_data.parseFile(cs_path)
## is there a generator containing additional information?
if not generator:
## all content in one flush
result_data = cs_data.render().splitlines()
## remove empty leading lines (avoids html warnings)
while not result_data[0].strip():
del result_data[0]
yield "\n".join(result_data)
else:
content_generate = generator()
dummy_line = """<!-- CONTENT_DUMMY -->"""
## now we do it linewise - checking for the content marker
for line in cs_data.render().splitlines():
if line.find(dummy_line) != -1:
yield line.replace(dummy_line, content_generate.next())
else:
yield line + "\n"