123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- #-*- coding: utf-8 -*-
-
- import sys
- import os.path
- import re
- import socket
- import inspect
- import copy
- from lodel.context import LodelContext
-
- from lodel.mlnamedobject.mlnamedobject import MlNamedObject
- from lodel.exceptions import LodelException, LodelExceptions, LodelFatalError, FieldValidationError
-
-
- ##
- # @package lodel.settings.validator Lodel2 settings validators/cast module.
- #
- # Validator are registered in the Validator class.
- # @note to get a list of registered default validators just run
- # <pre>$ python scripts/settings_validator.py</pre>
- #
- # @remarks Should we reconsider specifying conf right in this module?
-
-
- ##
- # @brief Exception class that should be raised when a validation fails
- class ValidationError(Exception):
- pass
-
-
- ##
- # @brief Handles settings validators
- #
- # Class instance are callable objects that takes a value argument (the value
- # to validate). It raises a ValidationError if validation fails, else it returns
- # a properly cast value.
- #
- #@todo Implement an IP validator for use in the multisite confspec
- class Validator(MlNamedObject):
-
- _validators = dict()
- _description = dict()
-
- ##
- # @brief Instantiate a validator
- #
- #@param name str: validator name
- #@param none_is_valid bool: if True None will be validated
- #@param **kwargs: more arguments for the validator
- def __init__(self, name, none_is_valid=False, display_name=None, help_text=None, **kwargs):
- if name is not None and name not in self._validators:
- raise LodelFatalError("No validator named '%s'" % name)
- self.__none_is_valid = none_is_valid
- self.__name = name
- self._opt_args = kwargs
- if display_name is None:
- display_name = name
- super().__init__(display_name, help_text)
-
- ##
- # @brief Calls the validator.
- #
- # @param value mixed:
- # @return mixed: The properly casted value
- # @throw ValidationError
- def __call__(self, value):
- if value is None:
- if self.__none_is_valid:
- return None
- else:
- raise ValidationError('None is not a valid value')
- if self.__none_is_valid and value is None:
- return None
- try:
- ret = self._validators[self.__name](value, **self._opt_args)
- return ret
- except Exception as exp:
- raise ValidationError(exp)
-
- ##
- # @brief Register a new validator
- #
- # @param name string: validator name
- # @param callback callable: the function that will validate a value
- # @param description string:
- @classmethod
- def register_validator(cls, name, callback, description=None):
- if name in cls._validators:
- raise NameError("A validator named '%s' allready exists" % name)
- ##
- # @todo Broken test for callable.
- if not inspect.isfunction(callback) and not inspect.ismethod(callback) and not hasattr(callback, '__call__'):
- raise TypeError("Callable expected but got %s" % type(callback))
- cls._validators[name] = callback
- cls._description[name] = description
-
- ##
- # @brief Get the validator list associated with description
- @classmethod
- def validators_list(cls):
- return copy.copy(cls._description)
-
- ##
- # @brief Creates and registers an iterative list validator
- #
- # @param elt_validator callable: The validator that will be used to validate
- # each of the list values.
- # @param validator_name string:
- # @param description None | string:
- # @param separator string: The element separator.
- # @return A Validator instance.
- @classmethod
- def create_list_validator(cls, validator_name, elt_validator, description=None, separator=','):
- def list_validator(value):
- res = list()
- if value is None:
- return res
- errors = list()
- for elt in value.split(separator):
- elt = elt_validator(elt)
- if len(elt) > 0:
- res.append(elt)
- return res
- description = "Convert value to an array" if description is None else description
- cls.register_validator(validator_name, list_validator, description)
- return cls(validator_name)
-
- ##
- # @brief Creates and registers a list validator that reads an array
- # and returns a string
- # @param elt_validator callable: The validator that will be used to validate
- # each elt value
- # @param validator_name string:
- # @param description None | string:
- # @param separator string: The element separator
- # @return A Validator instance
- @classmethod
- def create_write_list_validator(cls, validator_name, elt_validator, description=None, separator=','):
- def write_list_validator(value):
- res = ''
- for elt in value:
- res += elt_validator(elt) + ','
- return res[:len(res) - 1]
- description = "Convert value to a string" if description is None else description
- cls.register_validator(validator_name, write_list_validator, description)
- return cls(validator_name)
-
- ##
- # @brief Create and register a regular expression validator
- #
- # @param pattern str: regex pattern
- # @param validator_name str: The validator name
- # @param description str: Validator description
- # @return a Validator instance
- @classmethod
- def create_re_validator(cls, pattern, validator_name, description=None):
- def re_validator(value):
- if not re.match(pattern, value):
- raise ValidationError(
- "The value '%s' doesn't match the following pattern '%s'"
- % pattern)
- return value
- # registering the validator
- cls.register_validator(validator_name, re_validator,
- ("Match value to '%s'" % pattern)
- if description is None else description)
- return cls(validator_name)
-
- ##
- # @return The list of registered validators.
- @classmethod
- def validators_list_str(cls):
- result = ''
- for name in sorted(cls._validators.keys()):
- result += "\t%016s" % name
- if name in cls._description and cls._description[name] is not None:
- result += ": %s" % cls._description[name]
- result += "\n"
- return result
-
-
- ##
- # @brief Integer value validator callback
- #
- # @remarks Is it intended that this function simply tries casting to an integer?
- # Is it also intended that it does not use any try/catch block?
- def int_val(value):
- return int(value)
-
-
- ##
- # @brief Output file validator callback
- #
- # @return A file object (if filename is '-' return sys.stderr)
- def file_err_output(value):
- if not isinstance(value, str):
- raise ValidationError("A string was expected but got '%s' " % value)
- if value == '-':
- return None
- return value
-
-
- ##
- # @brief Boolean validator callback
- def boolean_val(value):
- if isinstance(value, bool):
- return value
- if value.strip().lower() == 'true' or value.strip() == '1':
- value = True
- elif value.strip().lower() == 'false' or value.strip() == '0':
- value = False
- else:
- raise ValidationError("A boolean was expected but got '%s' " % value)
- return bool(value)
-
-
- ##
- # @brief Validate a directory path
- def directory_val(value):
- res = Validator('strip')(value)
- if not os.path.isdir(res):
- raise ValidationError("Following path don't exists or is not a directory : '%s'" % res)
- return res
-
-
- ##
- # @brief Validates a loglevel value
- def loglevel_val(value):
- valids = ['DEBUG', 'INFO', 'WARNING', 'SECURITY', 'ERROR', 'CRITICAL']
- if value.upper() not in valids:
- raise ValidationError(
- "The value '%s' is not a valid loglevel" % value)
- return value.upper()
-
-
- ##
- # @brief Validates a path
- #
- # @remarks is it intended to have both @ref directory_val and this function
- # right here?
- def path_val(value):
- if value is None or not os.path.exists(value):
- raise ValidationError(
- "path '%s' doesn't exists" % value)
- return value
-
-
- ##
- # @brief Validates None
- #
- # @remarks Purpose?
- def none_val(value):
- if value is None:
- return None
- raise ValidationError("This settings cannot be set in configuration file")
-
-
- ##
- # @brief Validates a string
- def str_val(value):
- try:
- return str(value)
- except Exception as exp:
- raise ValidationError("Can't to convert value to string: " + str(exp))
-
-
- ##
- # @brief Validate using a regex
- def regex_val(value, pattern):
- if re.match(pattern, value) is None:
- raise ValidationError("The value '%s' is not validated by : \
- r\"%s\"" % (value, pattern))
- return value
-
-
- ##
- # @brief Validate a hostname (ipv4 or ipv6)
- def host_val(value):
- if value == 'localhost':
- return value
- ok = False
- try:
- socket.inet_aton(value)
- return value
- except (TypeError, OSError):
- pass
- try:
- socket.inet_pton(socket.AF_INET6, value)
- return value
- except (TypeError, OSError):
- pass
- try:
- socket.getaddrinfo(value, 80)
- return value
- except (TypeError, socket.gaierror):
- msg = "The value '%s' is not a valid host"
- raise ValidationError(msg % value)
-
-
- def custom_list_validator(value, validator_name, validator_kwargs=None):
- validator_kwargs = dict() if validator_kwargs is None else validator_kwargs
- validator = Validator(validator_name, **validator_kwargs)
- for item in value.split():
- validator(item)
- return value.split()
-
-
- #
- # Default validators registration
- #
- Validator.register_validator('custom_list', custom_list_validator,
- 'A list validator that takes a "validator_name" as argument')
- Validator.register_validator('dummy', lambda value: value, 'Validate anything')
- Validator.register_validator('none', none_val, 'Validate None')
- Validator.register_validator('string', str_val, 'Validate string values')
- Validator.register_validator('strip', str.strip, 'String trim')
- Validator.register_validator('int', int_val, 'Integer value validator')
- Validator.register_validator('bool', boolean_val, 'Boolean value validator')
- Validator.register_validator('errfile', file_err_output,
- 'Error output file validator (return stderr if filename is "-")')
- Validator.register_validator('directory', directory_val,
- 'Directory path validator')
- Validator.register_validator('loglevel', loglevel_val, 'Loglevel validator')
- Validator.register_validator('path', path_val, 'path validator')
- Validator.register_validator('host', host_val, 'host validator')
- Validator.register_validator('regex', regex_val,
- 'RegEx name validator (take re as argument)')
- Validator.create_list_validator('list', Validator('strip'), description="Simple list validator. Validate a list of values separated by ','",
- separator=',')
- Validator.create_list_validator(
- 'directory_list',
- Validator('directory'),
- description="Validator for a list of directory path separated with ','",
- separator=',')
- Validator.create_write_list_validator(
- 'write_list',
- Validator('directory'),
- description="Validator for an array of values \
- which will be set in a string, separated by ','",
- separator=',')
- Validator.create_re_validator(
- r'^https?://[^\./]+.[^\./]+/?.*$',
- 'http_url',
- 'Url validator')
-
-
- ##
- # @brief Validator for Editorial Model components.
- #
- # Designed to validate a conf that indicates a class.field in an EM
- #
- # @todo modify the hardcoded dyncode import (it's a warning)
- def emfield_val(value):
- from lodel.plugin.hooks import LodelHook
- spl = value.split('.')
- if len(spl) != 2:
- msg = "Expected a value in the form CLASSNAME.FIELDNAME but got : %s"
- raise ValidationError(msg % value)
- value = tuple(spl)
- # Late validation hook
-
- @LodelHook('lodel2_dyncode_bootstraped')
- def emfield_conf_check(hookname, caller, payload):
- import leapi_dyncode as dyncode # <-- dirty & quick
- classnames = {cls.__name__.lower(): cls for cls in dyncode.dynclasses}
- if value[0].lower() not in classnames:
- msg = "Following dynamic class do not exists in current EM : %s"
- raise ValidationError(msg % value[0])
- ccls = classnames[value[0].lower()]
- if value[1].lower() not in ccls.fieldnames(True):
- msg = "Following field not found in class %s : %s"
- raise ValidationError(msg % value)
- return value
-
-
- ##
- # @brief Validator for plugin name & its type optionally
- #
- # Able to check that the value is a plugin and if it is of a specific type
- def plugin_validator(value, ptype=None):
- if value:
- from lodel.plugin.hooks import LodelHook
- value = copy.copy(value)
-
- @LodelHook('lodel2_dyncode_bootstraped')
- def plugin_type_checker(hookname, caller, payload):
- from lodel.plugin.plugins import Plugin
- from lodel.plugin.exceptions import PluginError
- if value is None:
- return
- try:
- plugin = Plugin.get(value)
- except PluginError:
- msg = "No plugin named %s found"
- msg %= value
- raise ValidationError(msg)
- if plugin._type_conf_name.lower() != ptype.lower():
- msg = "A plugin of type '%s' was expected but found a plugin \
- named '%s' that is a '%s' plugin"
- msg %= (ptype, value, plugin._type_conf_name)
- raise ValidationError(msg)
- return value
- return None
-
-
- Validator.register_validator(
- 'plugin',
- plugin_validator,
- 'plugin name & type validator')
- Validator.register_validator(
- 'emfield',
- emfield_val,
- 'EmField name validator')
-
-
- ##
- # Lodel 2 configuration specification
- #
- # @brief Append a piece of confspec
- #
- # @param orig dict : the confspec to update
- # @param section str : section name
- # @param key str
- # @param validator Validator : the validator to use to check this configuration key's value
- # @param default
- # @return new confspec
- #
- # @note orig is modified during the process
- def confspec_append(orig, section, key, validator, default):
- if section not in orig:
- orig[section] = dict()
- if key not in orig[section]:
- orig[section][key] = (default, validator)
- return orig
-
-
- ##
- # @brief Global specifications for lodel2 settings
- LODEL2_CONF_SPECS = {
- 'lodel2': {
- 'debug': (True, Validator('bool')),
- 'sitename': ('noname', Validator('strip')),
- 'runtest': (False, Validator('bool')),
- },
- 'lodel2.logging.*': {
- 'level': ('ERROR', Validator('loglevel')),
- 'context': (False, Validator('bool')),
- 'filename': ("-", Validator('errfile', none_is_valid=False)),
- 'backupcount': (5, Validator('int', none_is_valid=False)),
- 'maxbytes': (1024 * 10, Validator('int', none_is_valid=False)),
- },
- 'lodel2.editorialmodel': {
- 'emfile': ('em.pickle', Validator('strip')),
- 'emtranslator': ('picklefile', Validator('strip')),
- 'dyncode': ('leapi_dyncode.py', Validator('strip')),
- 'groups': ('', Validator('list')),
- 'editormode': (False, Validator('bool')),
- },
- 'lodel2.datasources.*': {
- 'read_only': (False, Validator('bool')),
- 'identifier': (None, Validator('string')),
- },
- 'lodel2.auth': {
- 'login_classfield': ('user.login', Validator('emfield')),
- 'pass_classfield': ('user.password', Validator('emfield')),
- },
- }
|