cryptonas/bin/WebInterfaceSites.py

427 lines
14 KiB
Python
Executable file

import CryptoBox
import WebInterfaceDataset
import re
import Plugins
from CryptoBoxExceptions import *
import cherrypy
import types
import os
try:
import neo_cgi, neo_util, neo_cs
except ImportError:
errorMsg = "Could not import clearsilver module. Try 'apt-get install python-clearsilver'."
self.log.error(errorMsg)
sys.stderr.write(errorMsg)
raise ImportError, errorMsg
class PluginIconHandler:
def __init__(self, plugins):
for plugin in plugins.getPlugins():
if not plugin: continue
plname = plugin.getName()
## expose the getIcon function of this plugin
setattr(self, plname, plugin.getIcon)
class WebInterfaceSites:
'''
'''
## this template is used under strange circumstances
defaultTemplate = "empty"
def __init__(self):
import logging
self.cbox = CryptoBox.CryptoBoxProps()
self.log = logging.getLogger("CryptoBox")
self.prefs = self.cbox.prefs
self.__resetDataset()
def __resetDataset(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.__loadPlugins()
self.dataset = WebInterfaceDataset.WebInterfaceDataset(self.cbox, self.prefs, self.pluginList.getPlugins())
## publish plugin icons
self.icons = PluginIconHandler(self.pluginList)
self.icons.exposed = True
## check, if a configuration partition has become available
self.cbox.prefs.preparePartition()
def __loadPlugins(self):
self.pluginList = Plugins.PluginManager(self.cbox, self.prefs["Locations"]["PluginDir"])
for plugin in self.pluginList.getPlugins():
if not plugin: continue
plname = plugin.getName()
if plugin.isEnabled():
self.cbox.log.info("Plugin '%s' loaded" % plname)
## this should be the "easiest" way to expose all plugins as URLs
setattr(self, plname, self.return_plugin_action(plugin))
setattr(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)
## this is a function decorator to check authentication
## it has to be defined before any page definition requiring authentification
def __requestAuth(self=None):
def check_credentials(site):
def _inner_wrapper(self, *args, **kargs):
import base64
## define a "non-allowed" function
user, password = None, None
try:
resp = cherrypy.request.headers["Authorization"][6:] # ignore "Basic "
(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
authDict = self.cbox.prefs.userDB["admins"]
if user in authDict.keys():
if self.cbox.prefs.userDB.getDigest(password) == authDict[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=""):
self.__resetDataset()
self.__setWebLang(weblang)
self.__checkEnvironment()
## do not forget the language!
param_dict = {"weblang":weblang}
## render "disks" plugin by default
return self.return_plugin_action(self.pluginList.getPlugin("disks"))(**param_dict)
def return_plugin_action(self, plugin):
def handler(self, **args):
self.__resetDataset()
self.__checkEnvironment()
args_orig = dict(args)
## set web interface language
try:
self.__setWebLang(args["weblang"])
del args["weblang"]
except KeyError:
self.__setWebLang("")
## we always read the "device" setting - otherwise volume-plugin links
## would not work easily (see "volume_props" linking to "format_fs")
## it will get ignored for non-volume plugins
try:
plugin.device = None
if self.__setDevice(args["device"]):
plugin.device = args["device"]
del args["device"]
except KeyError:
pass
## check the device argument of volume plugins
if "volume" in plugin.pluginCapabilities:
## initialize the dataset of the selected device if necessary
if plugin.device:
self.dataset.setCurrentDiskState(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 doAction function (e.g. useful for umount-before-format)
try:
if args["redirect"]:
override_nextTemplate = { "plugin":args["redirect"] }
if "volume" in plugin.pluginCapabilities:
override_nextTemplate["values"] = {"device":plugin.device}
del args["redirect"]
except KeyError:
override_nextTemplate = None
## call the plugin handler
nextTemplate = plugin.doAction(**args)
## for 'volume' plugins: reread the dataset of the current disk
## additionally: set the default template for plugins
if "volume" in plugin.pluginCapabilities:
## maybe the state of the current volume was changed?
self.dataset.setCurrentDiskState(plugin.device)
if not nextTemplate: nextTemplate = { "plugin":"volume_mount", "values":{"device":plugin.device}}
else:
## maybe a non-volume plugin changed some plugin settings (e.g. plugin_manager)
self.dataset.setPluginData()
## update the container hdf-dataset (maybe a plugin changed the state of a container)
self.dataset.setContainersState()
## default page for non-volume plugins is the disk selection
if not nextTemplate: nextTemplate = { "plugin":"disks", "values":{} }
## was a redirect requested?
if override_nextTemplate:
nextTemplate = override_nextTemplate
## if another plugins was choosen for 'nextTemplate', then do it!
if isinstance(nextTemplate, types.DictType) \
and "plugin" in nextTemplate.keys() \
and "values" in nextTemplate.keys() \
and self.pluginList.getPlugin(nextTemplate["plugin"]):
valueDict = dict(nextTemplate["values"])
## force the current weblang attribute - otherwise it gets lost
valueDict["weblang"] = self.dataset["Settings.Language"]
new_plugin = self.pluginList.getPlugin(nextTemplate["plugin"])
return self.return_plugin_action(new_plugin)(**valueDict)
## save the currently active plugin name
self.dataset["Data.ActivePlugin"] = plugin.getName()
return self.__render(nextTemplate, plugin)
## apply authentication?
if plugin.isAuthRequired():
return lambda **args: self.__requestAuth()(handler)(self, **args)
else:
return lambda **args: handler(self, **args)
## test authentication
@cherrypy.expose
@__requestAuth
def test(self, weblang=""):
self.__resetDataset()
self.__setWebLang(weblang)
self.__checkEnvironment()
return "test passed"
@cherrypy.expose
def test_stream(self):
"""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 a in range(10):
yield "<li>yes: %d - %s</li>" % (a, str(time.time()))
time.sleep(1)
yield "</ul></p></html>"
##################### input checker ##########################
def __checkEnvironment(self):
"""here we should place all interesting checks to inform the user of problems
examples are: non-https, readonly-config, ...
"""
## TODO: maybe add an option "mount"?
if self.cbox.prefs.requiresPartition() and not self.cbox.prefs.getActivePartition():
self.dataset["Data.EnvironmentWarning"] = "ReadOnlyConfig"
# TODO: turn this on soon (add "not") - for now it is annoying
if self.__checkHTTPS():
self.dataset["Data.EnvironmentWarning"] = "NoSSL"
def __checkHTTPS(self):
## check the request scheme
if cherrypy.request.scheme == "https": return True
## check an environment setting - this is quite common behind proxies
try:
if os.environ["HTTPS"]: return True
except KeyError:
pass
## check http header TODO (check pound for the name)
try:
if cherrypy.request.headers["TODO"]: return True
except KeyError:
pass
## the connection seems to be unencrypted
return False
def __setWebLang(self, value):
guess = value
availLangs = self.cbox.getAvailableLanguages()
## no language specified: check browser language
if not guess:
guess = self.__getPreferredBrowserLanguage(availLangs)
## no preferred language or invalid language?
if not guess \
or not guess in availLangs \
or re.search(u'\W', guess):
## warn only for invalid languages
if not guess is None:
self.cbox.log.info("invalid language choosen: %s" % guess)
guess = self.prefs["WebSettings"]["Language"]
## maybe the language is still not valid
if not guess in availLangs:
self.log.warn("the configured language is invalid: %s" % guess)
guess = "en"
## maybe there is no english dataset???
if not guess in availLangs:
self.log.warn("couldn't find the english dataset")
guess = availLangs[0]
self.dataset["Settings.Language"] = guess
## we only have to save it, if it was specified correctly and explicitly
if value == guess:
self.dataset["Settings.LinkAttrs.weblang"] = guess
def __getPreferredBrowserLanguage(self, availLangs):
"""guess the preferred language of the user (as sent by the browser)
take the first language, that is part of 'availLangs'
"""
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(u"\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 availLangs: 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 availLangs: return short_lang
## we give up
return None
def __setDevice(self, device):
if device and re.match(u'[\w /\-]+$', device) and self.cbox.getContainer(device):
self.log.debug("select device: %s" % device)
return True
else:
self.log.warn("invalid device: %s" % device)
self.dataset["Data.Warning"] = "InvalidDevice"
return False
def __checkVolumeName(self, name):
if name and re.match(u'[\w \-]+$', name):
return True
else:
return False
def __getLanguageValue(self, value):
hdf = self.__getLanguageData(self.dataset["Settings.Language"])
return hdf.getValue(value, "")
def __getLanguageData(self, web_lang="en"):
default_lang = "en"
conf_lang = self.prefs["WebSettings"]["Language"]
hdf = neo_util.HDF()
langDir = os.path.abspath(self.prefs["Locations"]["LangDir"])
langFiles = []
## first: read default language (en)
if (default_lang != conf_lang) and (default_lang != web_lang):
langFiles.append(os.path.join(langDir, default_lang + ".hdf"))
## second: read language as defined in the config file
if (conf_lang != web_lang):
langFiles.append(os.path.join(langDir, conf_lang + ".hdf"))
## third: read language as configured via web interface
langFiles.append(os.path.join(langDir, web_lang + ".hdf"))
for langFile in langFiles:
if os.access(langFile, os.R_OK):
hdf.readFile(langFile)
else:
log.warn("Couldn't read language file: %s" % langFile)
return hdf
def __render(self, renderInfo, plugin=None):
'''renders from clearsilver templates and returns the resulting html
'''
## is renderInfo a string (filename of the template) or a dictionary?
if type(renderInfo) == types.DictType:
template = renderInfo["template"]
if renderInfo.has_key("generator"):
generator = renderInfo["generator"]
else:
generator = False
else:
(template, generator) = (renderInfo, None)
## load the language data
hdf = neo_util.HDF()
hdf.copy("Lang", self.__getLanguageData(self.dataset["Settings.Language"]))
## first: assume, that the template file is in the global template directory
self.dataset["Settings.TemplateFile"] = os.path.abspath(os.path.join(self.prefs["Locations"]["TemplateDir"], template + ".cs"))
if plugin:
## check, if the plugin provides the template file -> overriding
plugin_cs_file = plugin.getTemplateFileName(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.getName()] = plugin.getStatus()
## load the language data
pl_lang = plugin.getLanguageData(self.dataset["Settings.Language"])
if pl_lang:
hdf.copy("Lang.Plugins.%s" % plugin.getName(), pl_lang)
## load the dataset of the plugin
plugin.loadDataSet(hdf)
self.log.info("rendering site: " + template)
cs_path = os.path.abspath(os.path.join(self.prefs["Locations"]["TemplateDir"], "main.cs"))
if not os.access(cs_path, os.R_OK):
log.error("Couldn't read clearsilver file: %s" % cs_path)
yield "Couldn't read clearsilver file: %s" % cs_path
return
self.log.debug(self.dataset)
for key in self.dataset.keys():
hdf.setValue(key,str(self.dataset[key]))
cs = neo_cs.CS(hdf)
cs.parseFile(cs_path)
## is there a generator containing additional information?
if generator is None:
## all content in one flush
yield cs.render()
else:
content_generate = generator()
dummy_line = """<!-- CONTENT_DUMMY -->"""
## now we do it linewise - checking for the content marker
for line in cs.render().splitlines():
if line.find(dummy_line) != -1:
yield line.replace(dummy_line, content_generate.next())
else:
yield line + "\n"