#
# 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
#

"""Manage the configuration of a CryptoBox
"""

__revision__ = "$Id$"

from cryptobox.core.exceptions import *
import logging
import subprocess
import os
import configobj, validate
import syslog


CONF_LOCATIONS = [
    "./cryptobox.conf",
    "~/.cryptobox.conf",
    "/etc/cryptobox-server/cryptobox.conf"]

VOLUMESDB_FILE = "cryptobox_volumes.db"
PLUGINCONF_FILE = "cryptobox_plugins.conf"
USERDB_FILE = "cryptobox_users.db"

## allow to retrieve the most recently created setting object
CURRENT_SETTING = []


def get_current_settings():
    """return the most recently created setting object
    """
    if not CURRENT_SETTING:
        return None
    else:
        return CURRENT_SETTING[0]



class CryptoBoxSettings:
    """Manage the various configuration files of the CryptoBox
    """

    def __init__(self, config_file=None):
        self.__is_initialized = False
        self.log = logging.getLogger("CryptoNAS")
        config_file = self.__get_config_filename(config_file)
        self.log.info("loading config file: %s" % config_file)
        self.prefs = self.__get_preferences(config_file)
        if not "PluginSettings" in self.prefs:
            self.prefs["PluginSettings"] = {}
        self.__validate_config()
        self.__configure_log_handler()
        self.__check_unknown_preferences()
        self.prepare_partition()
        self.volumes_db = self.__get_volumes_database()
        self.plugin_conf = self.__get_plugin_config()
        self.user_db = self.__get_user_db()
        self.misc_files = []
        self.reload_misc_files()
        self.__is_initialized = True
        CURRENT_SETTING.insert(0, self)
    

    def reload_misc_files(self):
        """Call this method after creating or removing a 'misc' configuration file
        """
        self.misc_files = self.__get_misc_files()
    

    def write(self):
        """
        write all local setting files including the content of the "misc" subdirectory
        """
        status = True
        try:
            self.volumes_db.write()
        except IOError:
            self.log.warn("Could not save the volume database")
            status = False
        try:
            self.plugin_conf.write()
        except IOError:
            self.log.warn("Could not save the plugin configuration")
            status = False
        try:
            self.user_db.write()
        except IOError:
            self.log.warn("Could not save the user database")
            status = False
        for misc_file in self.misc_files:
            if not misc_file.save():
                self.log.warn("Could not save a misc setting file (%s)" % misc_file.filename)
                status = False
        return status


    def get_misc_config_filename(self, name):
        """Return an absolute filename for a given filename 'name'

        'name' should not contain slashes (no directory part!)
        """
        return os.path.join(self.prefs["Locations"]["SettingsDir"], "misc", name)
    

    def create_misc_config_file(self, name, content):
        """Create a new configuration file in the 'settings' directory

        "name" should be the basename (without a directory)
        "content" will be directly written to the file
        this method may throw an IOException
        """
        misc_conf_file = self.get_misc_config_filename(name)
        misc_conf_dir = os.path.dirname(misc_conf_file)
        if not os.path.isdir(misc_conf_dir):
            try:
                os.mkdir(misc_conf_dir)
            except OSError, err_msg:
                ## the caller expects only IOError
                raise IOError, err_msg
        cfile = open(misc_conf_file, "w")
        try:
            cfile.write(content)
        except IOError:
            cfile.close()
            raise
        cfile.close()
        ## reread all misc files automatically - this should be ok
        self.reload_misc_files()


    def requires_partition(self):
        return bool(self.prefs["Main"]["UseConfigPartition"])
    

    def get_active_partition(self):
        """Return the currently active cnfiguration partition.
        """
        settings_dir = self.prefs["Locations"]["SettingsDir"]
        if not os.path.ismount(settings_dir):
            return None
        for line in file("/proc/mounts"):
            fields = line.split(" ")
            mount_dir = fields[1]
            fs_type = fields[2]
            if fs_type == "tmpfs":
                ## skip ramdisks - these are not really "active partitions"
                continue
            try:
                if os.path.samefile(mount_dir, settings_dir):
                    return fields[0]
            except OSError:
                pass
        ## no matching entry found
        return None


    def mount_partition(self):
        """Mount a config partition.
        """
        self.log.debug("trying to mount configuration partition")
        if not self.requires_partition():
            self.log.warn("mountConfigPartition: configuration partition is "
                    + "not required - mounting anyway")
        if self.get_active_partition():
            self.log.warn("mountConfigPartition: configuration partition already "
                    + "mounted - not mounting again")
            return False
        conf_partitions = self.get_available_partitions()
        mount_dir = self.prefs["Locations"]["SettingsDir"]
        if not conf_partitions:
            ## return, if tmpfs is already mounted
            if os.path.ismount(mount_dir):
                self.log.info("A ramdisk seems to be already mounted as a config " \
                        + "partition - doing nothing ...")
                ## return without any actions
                return True
            self.log.warn("no configuration partition found - you have to create "
                    + "it first")
            ## mount tmpfs instead to provide a place for storing stuff
            ## "_tmpfs_" as parameter for mount is interpreted as a magic word
            ## by CryptoBoxRootActions
            proc = subprocess.Popen(
                shell = False,
                stdout = subprocess.PIPE,
                stderr = subprocess.PIPE,
                args = [
                    self.prefs["Programs"]["super"],
                    self.prefs["Programs"]["CryptoBoxRootActions"],
                    "program", "mount",
                    "_tmpfs_",
                    mount_dir ])
            (stdout, stderr) = proc.communicate()
            if proc.returncode != 0:
                self.log.error("Failed to mount a ramdisk for storing settings: %s" \
                        % stderr)
                return False
            self.log.info("Ramdisk (tmpfs) mounted as config partition ...")
        else:
            partition = conf_partitions[0]
            ## umount tmpfs in case it is active
            if os.path.ismount(mount_dir):
                self.umount_partition()
            proc = subprocess.Popen(
                shell = False,
                stdout = subprocess.PIPE,
                stderr = subprocess.PIPE,
                args = [
                    self.prefs["Programs"]["super"],
                    self.prefs["Programs"]["CryptoBoxRootActions"],
                    "program", "mount",
                    partition,
                    mount_dir ])
            (stdout, stderr) = proc.communicate()
            if proc.returncode != 0:
                self.log.error("Failed to mount the configuration partition (%s): %s" % \
                        (partition, stderr))
                return False
            self.log.info("configuration partition mounted: %s" % partition)
        ## write config files (not during first initialization of this object)
        if self.__is_initialized:
            self.write()
        return True


    def umount_partition(self):
        """Umount the currently active configuration partition.
        """
        mount_dir = self.prefs["Locations"]["SettingsDir"]
        if not os.path.ismount(mount_dir):
            self.log.warn("umountConfigPartition: no configuration partition mounted")
            return False
        self.reload_misc_files()
        proc = subprocess.Popen(
            shell = False,
            stdout = subprocess.PIPE,
            stderr = subprocess.PIPE,
            args = [
                self.prefs["Programs"]["super"],
                self.prefs["Programs"]["CryptoBoxRootActions"],
                "program", "umount",
                mount_dir ])
        (stdout, stderr) = proc.communicate()
        if proc.returncode != 0:
            self.log.error("Failed to unmount the configuration partition: %s" % stderr)
            return False
        self.log.info("configuration partition unmounted")
        return True
        

    def get_available_partitions(self):
        """returns a sequence of found config partitions"""
        self.log.debug("Retrieving available configuration partitions ...")
        proc = subprocess.Popen(
            shell = False,
            stdout = subprocess.PIPE,
            stderr = subprocess.PIPE,
            args = [
                self.prefs["Programs"]["blkid"],
                "-c", os.path.devnull,
                "-t", "LABEL=%s" % self.prefs["Main"]["ConfigVolumeLabel"] ])
        (output, error) = proc.communicate()
        if proc.returncode == 2:
            self.log.info("No configuration partitions found")
            return []
        elif proc.returncode == 4:
            self.log.warn("Failed to call 'blkid' for unknown reasons.")
            return []
        elif proc.returncode == 0:
            if output:
                return [e.strip().split(":", 1)[0] for e in output.splitlines()]
            else:
                return []
        else:
            self.log.warn("Unknown exit code of 'blkid': %d - %s" \
                    % (proc.returncode, error))

    
    def prepare_partition(self):
        """Mount a config partition if necessary.
        """
        if self.requires_partition() and not self.get_active_partition():
            self.mount_partition()


    def __getitem__(self, key):
        """redirect all requests to the 'prefs' attribute"""
        return self.prefs[key]


    def __get_preferences(self, config_file):
        """Load the CryptoBox configuration.
        """
        import StringIO
        config_rules = StringIO.StringIO(self.validation_spec)
        try:
            prefs = configobj.ConfigObj(config_file, configspec=config_rules)
            if prefs:
                self.log.info("found config: %s" % prefs.items())
            else: 
                raise CBConfigUnavailableError(
                        "failed to load the config file: %s" % config_file)
        except IOError, err_msg:
            raise CBConfigUnavailableError(
                    "unable to open the config file (%s): %s" % \
                            (config_file, err_msg))
        except configobj.ConfigObjError, err_msg:
            raise CBConfigError("failed to load config file (%s): %s" % \
                (config_file, err_msg))
        return prefs


    def __validate_config(self):
        """Check the configuration settings and cast value types.
        """
        result = self.prefs.validate(CryptoBoxSettingsValidator(), preserve_errors=True)
        error_list = configobj.flatten_errors(self.prefs, result)
        if not error_list:
            return
        error_msgs = []
        for sections, key, text in error_list:
            section_name = "->".join(sections)
            if not text:
                error_msg = "undefined configuration value (%s) in section '%s'" % \
                        (key, section_name)
            else:
                error_msg = "invalid configuration value (%s) in section '%s': %s" % \
                        (key, section_name, text)
            error_msgs.append(error_msg)
        raise CBConfigError, "\n".join(error_msgs)


    def __check_unknown_preferences(self):
        """Check the configuration file for unknown settings to avoid spelling mistakes.
        """
        import StringIO
        config_rules = configobj.ConfigObj(StringIO.StringIO(self.validation_spec),
                list_values=False)
        self.__recursive_section_check("", self.prefs, config_rules)
        
    
    def __recursive_section_check(self, section_path, section_config, section_rules):
        """should be called by '__check_unknown_preferences' for every section
        sends a warning message to the logger for every undefined (see validation_spec)
        configuration setting
        """
        for section in section_config.keys():
            element_path = section_path + section
            if section in section_rules.keys():
                if isinstance(section_config[section], configobj.Section):
                    if isinstance(section_rules[section], configobj.Section):
                        self.__recursive_section_check(element_path + "->",
                                section_config[section], section_rules[section])
                    else:
                        self.log.warn("configuration setting should be a value "
                                + "instead of a section name: %s" % element_path)
                else:
                    if not isinstance(section_rules[section], configobj.Section):
                        pass	# good - the setting is valid
                    else:
                        self.log.warn("configuration setting should be a section "
                                + "name instead of a value: %s" % element_path)
            elif element_path.startswith("PluginSettings->"):
                ## ignore plugin settings
                pass
            else:
                self.log.warn("unknown configuration setting: %s" % element_path)


    def __get_plugin_config(self):
        """Load the plugin configuration file if it exists.
        """
        import StringIO
        plugin_rules = StringIO.StringIO(self.pluginValidationSpec)
        try:
            try:
                plugin_conf_file = os.path.join(
                        self.prefs["Locations"]["SettingsDir"], PLUGINCONF_FILE)
            except KeyError:
                raise CBConfigUndefinedError("Locations", "SettingsDir")
        except SyntaxError:
            raise CBConfigInvalidValueError("Locations", "SettingsDir", plugin_conf_file,
                    "failed to interprete the filename of the plugin config file "
                    + "correctly (%s)" % plugin_conf_file)
        ## create plugin_conf_file if necessary
        if os.path.exists(plugin_conf_file):
            plugin_conf = configobj.ConfigObj(plugin_conf_file, configspec=plugin_rules)
        else:
            try:
                plugin_conf = configobj.ConfigObj(plugin_conf_file,
                        configspec=plugin_rules, create_empty=True)
            except IOError:
                plugin_conf = configobj.ConfigObj(configspec=plugin_rules)
                plugin_conf.filename = plugin_conf_file
        ## validate and convert values according to the spec
        plugin_conf.validate(validate.Validator())
        return plugin_conf


    def __get_volumes_database(self):
        """Load the volume database file if it exists.
        """
        #TODO: add configuration specification and validation [a]: -v
        try:
            try:
                conf_file = os.path.join(
                        self.prefs["Locations"]["SettingsDir"], VOLUMESDB_FILE)
            except KeyError:
                raise CBConfigUndefinedError("Locations", "SettingsDir")
        except SyntaxError:
            raise CBConfigInvalidValueError("Locations", "SettingsDir", conf_file,
                    "failed to interprete the filename of the volume database "
                    + "correctly (%s)" % conf_file)
        ## create conf_file if necessary
        if os.path.exists(conf_file):
            conf = configobj.ConfigObj(conf_file)
        else:
            try:
                conf = configobj.ConfigObj(conf_file, create_empty=True)
            except IOError:
                conf = configobj.ConfigObj()
                conf.filename = conf_file
        return conf


    def __get_user_db(self):
        """Load the user database file if it exists.
        """
        import StringIO
        try:
            # hashlib is available since python2.5
            import hashlib
            get_hash_obj = lambda text: hashlib.sha1(text)
        except ImportError:
            # sha is deprecated since python2.6
            import sha
            get_hash_obj = lambda text: sha.new(text)
        user_db_rules = StringIO.StringIO(self.userDatabaseSpec)
        try:
            try:
                user_db_file = os.path.join(
                        self.prefs["Locations"]["SettingsDir"], USERDB_FILE)
            except KeyError:
                raise CBConfigUndefinedError("Locations", "SettingsDir")
        except SyntaxError:
            raise CBConfigInvalidValueError("Locations", "SettingsDir", user_db_file,
                    "failed to interprete the filename of the users database file "
                    + "correctly (%s)" % user_db_file)
        ## create user_db_file if necessary
        if os.path.exists(user_db_file):
            user_db = configobj.ConfigObj(user_db_file, configspec=user_db_rules)
        else:
            try:
                user_db = configobj.ConfigObj(user_db_file,
                        configspec=user_db_rules, create_empty=True)
            except IOError:
                user_db = configobj.ConfigObj(configspec=user_db_rules)
                user_db.filename = user_db_file
        ## validate and set default value for "admin" user
        user_db.validate(validate.Validator())
        ## define password hash function - never use "sha" directly - SPOT
        user_db.get_digest = lambda password: get_hash_obj(password).hexdigest()
        return user_db


    def __get_misc_files(self):
        """Load miscelleanous configuration files.

        e.g.: an ssl certificate, ...
        """
        misc_dir = os.path.join(self.prefs["Locations"]["SettingsDir"], "misc")
        if (not os.path.isdir(misc_dir)) or (not os.access(misc_dir, os.X_OK)):
            return []
        misc_files = []
        for root, dirs, files in os.walk(misc_dir):
            misc_files.extend([os.path.join(root, e) for e in files])
        return [MiscConfigFile(os.path.join(misc_dir, f), self.log) for f in misc_files]


    def __get_config_filename(self, config_file):
        """Search for the configuration file.
        """
        import types
        if config_file is None:
            # no config file was specified - we will look for it in the ususal locations
            conf_file_list = [os.path.expanduser(f)
                for f in CONF_LOCATIONS
                if os.path.exists(os.path.expanduser(f))]
            if not conf_file_list:
                # no possible config file found in the usual locations
                raise CBConfigUnavailableError()
            config_file = conf_file_list[0]
        else:
            # a config file was specified (e.g. via command line)
            if type(config_file) != types.StringType:
                raise CBConfigUnavailableError(
                        "invalid config file specified: %s" % config_file)
            if not os.path.exists(config_file):
                raise CBConfigUnavailableError(
                        "could not find the specified configuration file (%s)" % config_file)
        return config_file


    def __configure_log_handler(self):
        """Configure the log handler of the CryptoBox according to the config.
        """
        log_level = self.prefs["Log"]["Level"].upper()
        log_level_avail = ["DEBUG", "INFO", "WARN", "ERROR"]
        if not log_level in log_level_avail:
            raise CBConfigInvalidValueError("Log", "Level", log_level,
                    "invalid log level: only %s are allowed" % str(log_level_avail))
        log_destination = self.prefs["Log"]["Destination"].lower()
        ## keep this in sync with the spec and the log_destination branches below
        log_dest_avail = ['file', 'syslog']
        if not log_destination in log_dest_avail:
            raise CBConfigInvalidValueError("Log", "Destination", log_destination,
                    "invalid log destination: only %s are allowed" % str(log_dest_avail))
        if log_destination == 'file':
            try:
                log_handler = logging.FileHandler(self.prefs["Log"]["Details"])
            except IOError:
                raise CBEnvironmentError("could not write to log file (%s)" % \
                        self.prefs["Log"]["Details"])
            log_handler.setFormatter(
                    logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
        elif log_destination == 'syslog':
            log_facility = self.prefs["Log"]["Details"].upper()
            log_facil_avail = ['KERN', 'USER', 'MAIL', 'DAEMON', 'AUTH', 'SYSLOG',
                    'LPR', 'NEWS', 'UUCP', 'CRON', 'AUTHPRIV', 'LOCAL0', 'LOCAL1',
                    'LOCAL2', 'LOCAL3', 'LOCAL4', 'LOCAL5', 'LOCAL6', 'LOCAL7']
            if not log_facility in log_facil_avail:
                raise CBConfigInvalidValueError("Log", "Details", log_facility,
                        "invalid log details for 'syslog': only %s are allowed" % \
                        str(log_facil_avail))
            ## retrive the log priority from the syslog module
            log_handler = LocalSysLogHandler("CryptoNAS",
                    getattr(syslog, 'LOG_%s' % log_facility))
            log_handler.setFormatter(
                    logging.Formatter('%(asctime)s CryptoNAS %(levelname)s: %(message)s'))
        else:
            ## this should never happen - we just have it in case someone forgets
            ## to update the spec, the 'log_dest_avail' or the above branches
            raise CBConfigInvalidValueError("Log", "Destination", log_destination,
                    "invalid log destination: only %s are allowed" % str(log_dest_avail))
        cbox_log = logging.getLogger("CryptoNAS")
        ## remove previous handlers (from 'basicConfig')
        cbox_log.handlers = []
        ## add new one
        cbox_log.addHandler(log_handler)
        ## do not call parent's handlers
        cbox_log.propagate = False
        ## 'log_level' is a string -> use 'getattr'
        cbox_log.setLevel(getattr(logging, log_level))
        ## the logger named "CryptoNAS" is configured now


    # We can't use real default values for the "directory_exists" tests below.
    # Otherwise configobj complains about the "invalid" default value (if the
    # directory does not exist) - even if the default value is not used.
    # Up to configobj version 4.3.2 this workaround was not necessary.
    validation_spec = """
[Main]
AllowedDevices = listOfDevices(default="/dev/invalid")
DefaultVolumePrefix = string(min=1)
DefaultCipher = string(default="aes-cbc-essiv:sha256")
ConfigVolumeLabel = string(min=1, default="cbox_config")
UseConfigPartition = integer(min=0, max=1, default=0)
DisabledPlugins = listOfPlugins(default=list())

[Locations]
MountParentDir = directoryMountExists(default=None)
SettingsDir = directorySettingsExists(default=None)
TemplateDir = directoryTemplateExists(default=None)
DocDir = directoryDocExists(default=None)
PluginDir = listOfExistingPluginDirectories(default=None)
EventDir = string(default="/etc/cryptobox-server/events.d")

[Log]
Level = option("debug", "info", "warn", "error", default="warn")
Destination = option("file", "syslog", default="file")
Details = string(min=1, default="/var/log/cryptobox-server/cryptobox.log")

[WebSettings]
Stylesheet = string(min=1)
Languages = listOfLanguages(default="en")

[Programs]
cryptsetup = fileExecutable(default="/sbin/cryptsetup")
mkfs = fileExecutable(default="/sbin/mkfs")
nice = fileExecutable(default="/usr/bin/nice")
blkid = fileExecutable(default="/sbin/blkid")
blockdev = fileExecutable(default="/sbin/blockdev")
mount = fileExecutable(default="/bin/mount")
umount = fileExecutable(default="/bin/umount")
super = fileExecutable(default="/usr/bin/super")
# this is the "program" name as defined in /etc/super.tab
CryptoBoxRootActions = string(min=1)

[PluginSettings]
[[__many__]]
        """

    pluginValidationSpec = """
[__many__]
visibility = boolean(default=None)
requestAuth = boolean(default=None)
rank = integer(default=None)
        """
    
    userDatabaseSpec = """
[admins]
admin = string(default=d033e22ae348aeb5660fc2140aec35850c4da997)
        """
    

class CryptoBoxSettingsValidator(validate.Validator):
    """Some custom configuration check functions.
    """

    def __init__(self):
        validate.Validator.__init__(self)
        self.functions["directoryMountExists"] = \
                self.check_mount_directory_exists
        self.functions["directorySettingsExists"] = \
                self.check_settings_directory_exists
        self.functions["directoryTemplateExists"] = \
                self.check_template_directory_exists
        self.functions["directoryDocExists"] = \
                self.check_doc_directory_exists
        self.functions["fileExecutable"] = self.check_file_executable
        self.functions["fileWriteable"] = self.check_file_writeable
        self.functions["listOfExistingPluginDirectories"] \
                = self.check_existing_plugin_directories
        self.functions["listOfLanguages"] = self.list_languages
        self.functions["listOfDevices"] = self.list_devices
        self.functions["listOfPlugins"] = self.list_plugins
    

    def check_mount_directory_exists(self, value):
        """Is the mount directory accessible?
        """
        # use the default path, if the setting is missing
        if value is None:
            value = "/var/cache/cryptobox-server/mnt"
        dir_path = os.path.abspath(value)
        if not os.path.isdir(dir_path):
            raise validate.VdtValueError("%s (not found)" % value)
        if not os.access(dir_path, os.X_OK):
            raise validate.VdtValueError("%s (access denied)" % value)
        return dir_path


    def check_settings_directory_exists(self, value):
        """Is the settings directory accessible?
        """
        # use the default path, if the setting is missing
        if value is None:
            value = "/var/cache/cryptobox-server/settings"
        dir_path = os.path.abspath(value)
        if not os.path.isdir(dir_path):
            raise validate.VdtValueError("%s (not found)" % value)
        if not os.access(dir_path, os.X_OK):
            raise validate.VdtValueError("%s (access denied)" % value)
        return dir_path


    def check_template_directory_exists(self, value):
        """Is the template directory accessible?
        """
        # use the default path, if the setting is missing
        if value is None:
            value = "/usr/share/cryptobox-server/templates"
        dir_path = os.path.abspath(value)
        if not os.path.isdir(dir_path):
            raise validate.VdtValueError("%s (not found)" % value)
        if not os.access(dir_path, os.X_OK):
            raise validate.VdtValueError("%s (access denied)" % value)
        return dir_path
    

    def check_doc_directory_exists(self, value):
        """Is the documentation directory accessible?
        """
        # use the default path, if the setting is missing
        if value is None:
            value = "/usr/share/doc/cryptobox-server/html"
        dir_path = os.path.abspath(value)
        if not os.path.isdir(dir_path):
            raise validate.VdtValueError("%s (not found)" % value)
        if not os.access(dir_path, os.X_OK):
            raise validate.VdtValueError("%s (access denied)" % value)
        return dir_path
    

    def check_file_executable(self, value):
        """Is the file executable?
        """
        file_path = os.path.abspath(value)
        if not os.path.isfile(file_path):
            raise validate.VdtValueError("%s (not found)" % value)
        if not os.access(file_path, os.X_OK):
            raise validate.VdtValueError("%s (access denied)" % value)
        return file_path
    

    def check_file_writeable(self, value):
        """Is the file writeable?
        """
        file_path = os.path.abspath(value)
        if os.path.isfile(file_path):
            if not os.access(file_path, os.W_OK):
                raise validate.VdtValueError("%s (not found)" % value)
        else:
            parent_dir = os.path.dirname(file_path)
            if os.path.isdir(parent_dir) and os.access(parent_dir, os.W_OK):
                return file_path
            raise validate.VdtValueError("%s (directory does not exist)" % value)
        return file_path


    def check_existing_plugin_directories(self, value):
        """Are these directories accessible?
        """
        # return the default value, if the settings is missing
        if value is None:
            value = ["/usr/share/cryptobox-server/plugins"]
        if not value:
            raise validate.VdtValueError("no plugin directory specified")
        if not isinstance(value, list):
            value = [value]
        result = []
        for one_dir in value:
            dir_path = os.path.abspath(one_dir)
            if not os.path.isdir(dir_path):
                raise validate.VdtValueError(
                        "%s (plugin directory not found)" % one_dir)
            if not os.access(dir_path, os.X_OK):
                raise validate.VdtValueError(
                        "%s (access denied for plugin directory)" % one_dir)
            result.append(dir_path)
        return result

    def list_languages(self, langs):
        """Return languages as a list.
        """
        if not langs:
            raise validate.VdtValueError("no language specified")
        if not isinstance(langs, list):
            langs = [langs]
        return langs

    def list_devices(self, devices):
        """Return devices as a list.
        """
        if not devices:
            raise validate.VdtValueError("no device specified")
        if not isinstance(devices, list):
            devices = [devices]
        return devices

    def list_plugins(self, plugins):
        """Return plugin names as a list.
        """
        if not plugins:
            plugins = []
        if isinstance(plugins, basestring):
            plugins = [plugins]
        elif not isinstance(plugins, list):
            raise validate.VdtValueError("invalid list of disabled plugins")
        return plugins



class MiscConfigFile:
    """all other config files (e.g. a ssl certificate) to be stored"""

    maxSize = 20480

    def __init__(self, filename, logger):
        self.filename = filename
        self.log = logger
        self.content = None
        self.load()

    
    def load(self):
        """Load a configuration file into memory.
        """
        fdesc = open(self.filename, "rb")
        ## limit the maximum size
        self.content = fdesc.read(self.maxSize)
        if fdesc.tell() == self.maxSize:
            self.log.warn("file in misc settings directory (" + str(self.filename) \
                    + ") is bigger than allowed (" + str(self.maxSize) + ")")
        fdesc.close()


    def save(self):
        """Save a configuration file to disk.
        """
        ## overriding of ro-files is not necessary (e.g. samba-include.conf)
        if os.path.exists(self.filename) and not os.access(self.filename, os.W_OK):
            return True
        save_dir = os.path.dirname(self.filename)
        ## create the directory, if necessary
        if not os.path.isdir(save_dir):
            try:
                os.mkdir(save_dir)
            except IOError:
                return False
        ## save the content of the file
        try:
            fdesc = open(self.filename, "wb")
        except IOError:
            return False
        try:
            fdesc.write(self.content)
            fdesc.close()
            return True
        except IOError:
            fdesc.close()
            return False



class LocalSysLogHandler(logging.Handler):
    """Pass logging messages to a local syslog server without unix sockets.

    derived from: logging.SysLogHandler
    """

    def __init__(self, prepend='CryptoBox', facility=syslog.LOG_USER):
        logging.Handler.__init__(self)
        self.formatter = None
        self.facility = facility
        syslog.openlog(prepend, 0, facility)
    

    def close(self):
        """close the syslog connection
        """
        syslog.closelog()
        logging.Handler.close(self)


    def emit(self, record):
        """format and send the log message
        """
        msg = "%s: %s" % (record.levelname, record.getMessage())
        try:
            syslog.syslog(record.levelno, msg)
        except Exception:
            self.handleError(record)

