No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

validator.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. #-*- coding: utf-8 -*-
  2. import sys
  3. import os.path
  4. import re
  5. import socket
  6. import inspect
  7. import copy
  8. from lodel.context import LodelContext
  9. from lodel.mlnamedobject.mlnamedobject import MlNamedObject
  10. from lodel.exceptions import LodelException, LodelExceptions, LodelFatalError, FieldValidationError
  11. ##
  12. # @package lodel.settings.validator Lodel2 settings validators/cast module.
  13. #
  14. # Validator are registered in the Validator class.
  15. # @note to get a list of registered default validators just run
  16. # <pre>$ python scripts/settings_validator.py</pre>
  17. #
  18. # @remarks Should we reconsider specifying conf right in this module?
  19. ##
  20. # @brief Exception class that should be raised when a validation fails
  21. class ValidationError(Exception):
  22. pass
  23. ##
  24. # @brief Handles settings validators
  25. #
  26. # Class instance are callable objects that takes a value argument (the value
  27. # to validate). It raises a ValidationError if validation fails, else it returns
  28. # a properly cast value.
  29. #
  30. #@todo Implement an IP validator for use in the multisite confspec
  31. class Validator(MlNamedObject):
  32. _validators = dict()
  33. _description = dict()
  34. ##
  35. # @brief Instantiate a validator
  36. #
  37. #@param name str: validator name
  38. #@param none_is_valid bool: if True None will be validated
  39. #@param **kwargs: more arguments for the validator
  40. def __init__(self, name, none_is_valid=False, display_name=None, help_text=None, **kwargs):
  41. if name is not None and name not in self._validators:
  42. raise LodelFatalError("No validator named '%s'" % name)
  43. self.__none_is_valid = none_is_valid
  44. self.__name = name
  45. self._opt_args = kwargs
  46. if display_name is None:
  47. display_name = name
  48. super().__init__(display_name, help_text)
  49. ##
  50. # @brief Calls the validator.
  51. #
  52. # @param value mixed:
  53. # @return mixed: The properly casted value
  54. # @throw ValidationError
  55. def __call__(self, value):
  56. if value is None:
  57. if self.__none_is_valid:
  58. return None
  59. else:
  60. raise ValidationError('None is not a valid value')
  61. if self.__none_is_valid and value is None:
  62. return None
  63. try:
  64. ret = self._validators[self.__name](value, **self._opt_args)
  65. return ret
  66. except Exception as exp:
  67. raise ValidationError(exp)
  68. ##
  69. # @brief Register a new validator
  70. #
  71. # @param name string: validator name
  72. # @param callback callable: the function that will validate a value
  73. # @param description string:
  74. @classmethod
  75. def register_validator(cls, name, callback, description=None):
  76. if name in cls._validators:
  77. raise NameError("A validator named '%s' allready exists" % name)
  78. ##
  79. # @todo Broken test for callable.
  80. if not inspect.isfunction(callback) and not inspect.ismethod(callback) and not hasattr(callback, '__call__'):
  81. raise TypeError("Callable expected but got %s" % type(callback))
  82. cls._validators[name] = callback
  83. cls._description[name] = description
  84. ##
  85. # @brief Get the validator list associated with description
  86. @classmethod
  87. def validators_list(cls):
  88. return copy.copy(cls._description)
  89. ##
  90. # @brief Creates and registers an iterative list validator
  91. #
  92. # @param elt_validator callable: The validator that will be used to validate
  93. # each of the list values.
  94. # @param validator_name string:
  95. # @param description None | string:
  96. # @param separator string: The element separator.
  97. # @return A Validator instance.
  98. @classmethod
  99. def create_list_validator(cls, validator_name, elt_validator, description=None, separator=','):
  100. def list_validator(value):
  101. res = list()
  102. if value is None:
  103. return res
  104. errors = list()
  105. for elt in value.split(separator):
  106. elt = elt_validator(elt)
  107. if len(elt) > 0:
  108. res.append(elt)
  109. return res
  110. description = "Convert value to an array" if description is None else description
  111. cls.register_validator(validator_name, list_validator, description)
  112. return cls(validator_name)
  113. ##
  114. # @brief Creates and registers a list validator that reads an array
  115. # and returns a string
  116. # @param elt_validator callable: The validator that will be used to validate
  117. # each elt value
  118. # @param validator_name string:
  119. # @param description None | string:
  120. # @param separator string: The element separator
  121. # @return A Validator instance
  122. @classmethod
  123. def create_write_list_validator(cls, validator_name, elt_validator, description=None, separator=','):
  124. def write_list_validator(value):
  125. res = ''
  126. for elt in value:
  127. res += elt_validator(elt) + ','
  128. return res[:len(res) - 1]
  129. description = "Convert value to a string" if description is None else description
  130. cls.register_validator(validator_name, write_list_validator, description)
  131. return cls(validator_name)
  132. ##
  133. # @brief Create and register a regular expression validator
  134. #
  135. # @param pattern str: regex pattern
  136. # @param validator_name str: The validator name
  137. # @param description str: Validator description
  138. # @return a Validator instance
  139. @classmethod
  140. def create_re_validator(cls, pattern, validator_name, description=None):
  141. def re_validator(value):
  142. if not re.match(pattern, value):
  143. raise ValidationError(
  144. "The value '%s' doesn't match the following pattern '%s'"
  145. % pattern)
  146. return value
  147. # registering the validator
  148. cls.register_validator(validator_name, re_validator,
  149. ("Match value to '%s'" % pattern)
  150. if description is None else description)
  151. return cls(validator_name)
  152. ##
  153. #  @return The list of registered validators.
  154. @classmethod
  155. def validators_list_str(cls):
  156. result = ''
  157. for name in sorted(cls._validators.keys()):
  158. result += "\t%016s" % name
  159. if name in cls._description and cls._description[name] is not None:
  160. result += ": %s" % cls._description[name]
  161. result += "\n"
  162. return result
  163. ##
  164. # @brief Integer value validator callback
  165. #
  166. # @remarks Is it intended that this function simply tries casting to an integer?
  167. # Is it also intended that it does not use any try/catch block?
  168. def int_val(value):
  169. return int(value)
  170. ##
  171. # @brief Output file validator callback
  172. #
  173. # @return A file object (if filename is '-' return sys.stderr)
  174. def file_err_output(value):
  175. if not isinstance(value, str):
  176. raise ValidationError("A string was expected but got '%s' " % value)
  177. if value == '-':
  178. return None
  179. return value
  180. ##
  181. # @brief Boolean validator callback
  182. def boolean_val(value):
  183. if isinstance(value, bool):
  184. return value
  185. if value.strip().lower() == 'true' or value.strip() == '1':
  186. value = True
  187. elif value.strip().lower() == 'false' or value.strip() == '0':
  188. value = False
  189. else:
  190. raise ValidationError("A boolean was expected but got '%s' " % value)
  191. return bool(value)
  192. ##
  193. # @brief Validate a directory path
  194. def directory_val(value):
  195. res = Validator('strip')(value)
  196. if not os.path.isdir(res):
  197. raise ValidationError("Following path don't exists or is not a directory : '%s'" % res)
  198. return res
  199. ##
  200. # @brief Validates a loglevel value
  201. def loglevel_val(value):
  202. valids = ['DEBUG', 'INFO', 'WARNING', 'SECURITY', 'ERROR', 'CRITICAL']
  203. if value.upper() not in valids:
  204. raise ValidationError(
  205. "The value '%s' is not a valid loglevel" % value)
  206. return value.upper()
  207. ##
  208. # @brief Validates a path
  209. #
  210. # @remarks is it intended to have both @ref directory_val and this function
  211. # right here?
  212. def path_val(value):
  213. if value is None or not os.path.exists(value):
  214. raise ValidationError(
  215. "path '%s' doesn't exists" % value)
  216. return value
  217. ##
  218. # @brief Validates None
  219. #
  220. # @remarks Purpose?
  221. def none_val(value):
  222. if value is None:
  223. return None
  224. raise ValidationError("This settings cannot be set in configuration file")
  225. ##
  226. # @brief Validates a string
  227. def str_val(value):
  228. try:
  229. return str(value)
  230. except Exception as exp:
  231. raise ValidationError("Can't to convert value to string: " + str(exp))
  232. ##
  233. # @brief Validate using a regex
  234. def regex_val(value, pattern):
  235. if re.match(pattern, value) is None:
  236. raise ValidationError("The value '%s' is not validated by : \
  237. r\"%s\"" % (value, pattern))
  238. return value
  239. ##
  240. # @brief Validate a hostname (ipv4 or ipv6)
  241. def host_val(value):
  242. if value == 'localhost':
  243. return value
  244. ok = False
  245. try:
  246. socket.inet_aton(value)
  247. return value
  248. except (TypeError, OSError):
  249. pass
  250. try:
  251. socket.inet_pton(socket.AF_INET6, value)
  252. return value
  253. except (TypeError, OSError):
  254. pass
  255. try:
  256. socket.getaddrinfo(value, 80)
  257. return value
  258. except (TypeError, socket.gaierror):
  259. msg = "The value '%s' is not a valid host"
  260. raise ValidationError(msg % value)
  261. def custom_list_validator(value, validator_name, validator_kwargs=None):
  262. validator_kwargs = dict() if validator_kwargs is None else validator_kwargs
  263. validator = Validator(validator_name, **validator_kwargs)
  264. for item in value.split():
  265. validator(item)
  266. return value.split()
  267. #
  268. # Default validators registration
  269. #
  270. Validator.register_validator('custom_list', custom_list_validator,
  271. 'A list validator that takes a "validator_name" as argument')
  272. Validator.register_validator('dummy', lambda value: value, 'Validate anything')
  273. Validator.register_validator('none', none_val, 'Validate None')
  274. Validator.register_validator('string', str_val, 'Validate string values')
  275. Validator.register_validator('strip', str.strip, 'String trim')
  276. Validator.register_validator('int', int_val, 'Integer value validator')
  277. Validator.register_validator('bool', boolean_val, 'Boolean value validator')
  278. Validator.register_validator('errfile', file_err_output,
  279. 'Error output file validator (return stderr if filename is "-")')
  280. Validator.register_validator('directory', directory_val,
  281. 'Directory path validator')
  282. Validator.register_validator('loglevel', loglevel_val, 'Loglevel validator')
  283. Validator.register_validator('path', path_val, 'path validator')
  284. Validator.register_validator('host', host_val, 'host validator')
  285. Validator.register_validator('regex', regex_val,
  286. 'RegEx name validator (take re as argument)')
  287. Validator.create_list_validator('list', Validator('strip'), description="Simple list validator. Validate a list of values separated by ','",
  288. separator=',')
  289. Validator.create_list_validator(
  290. 'directory_list',
  291. Validator('directory'),
  292. description="Validator for a list of directory path separated with ','",
  293. separator=',')
  294. Validator.create_write_list_validator(
  295. 'write_list',
  296. Validator('directory'),
  297. description="Validator for an array of values \
  298. which will be set in a string, separated by ','",
  299. separator=',')
  300. Validator.create_re_validator(
  301. r'^https?://[^\./]+.[^\./]+/?.*$',
  302. 'http_url',
  303. 'Url validator')
  304. ##
  305. # @brief Validator for Editorial Model components.
  306. #
  307. # Designed to validate a conf that indicates a class.field in an EM
  308. #
  309. # @todo modify the hardcoded dyncode import (it's a warning)
  310. def emfield_val(value):
  311. from lodel.plugin.hooks import LodelHook
  312. spl = value.split('.')
  313. if len(spl) != 2:
  314. msg = "Expected a value in the form CLASSNAME.FIELDNAME but got : %s"
  315. raise ValidationError(msg % value)
  316. value = tuple(spl)
  317. # Late validation hook
  318. @LodelHook('lodel2_dyncode_bootstraped')
  319. def emfield_conf_check(hookname, caller, payload):
  320. import leapi_dyncode as dyncode # <-- dirty & quick
  321. classnames = {cls.__name__.lower(): cls for cls in dyncode.dynclasses}
  322. if value[0].lower() not in classnames:
  323. msg = "Following dynamic class do not exists in current EM : %s"
  324. raise ValidationError(msg % value[0])
  325. ccls = classnames[value[0].lower()]
  326. if value[1].lower() not in ccls.fieldnames(True):
  327. msg = "Following field not found in class %s : %s"
  328. raise ValidationError(msg % value)
  329. return value
  330. ##
  331. # @brief Validator for plugin name & its type optionally
  332. #
  333. # Able to check that the value is a plugin and if it is of a specific type
  334. def plugin_validator(value, ptype=None):
  335. if value:
  336. from lodel.plugin.hooks import LodelHook
  337. value = copy.copy(value)
  338. @LodelHook('lodel2_dyncode_bootstraped')
  339. def plugin_type_checker(hookname, caller, payload):
  340. from lodel.plugin.plugins import Plugin
  341. from lodel.plugin.exceptions import PluginError
  342. if value is None:
  343. return
  344. try:
  345. plugin = Plugin.get(value)
  346. except PluginError:
  347. msg = "No plugin named %s found"
  348. msg %= value
  349. raise ValidationError(msg)
  350. if plugin._type_conf_name.lower() != ptype.lower():
  351. msg = "A plugin of type '%s' was expected but found a plugin \
  352. named '%s' that is a '%s' plugin"
  353. msg %= (ptype, value, plugin._type_conf_name)
  354. raise ValidationError(msg)
  355. return value
  356. return None
  357. Validator.register_validator(
  358. 'plugin',
  359. plugin_validator,
  360. 'plugin name & type validator')
  361. Validator.register_validator(
  362. 'emfield',
  363. emfield_val,
  364. 'EmField name validator')
  365. ##
  366. # Lodel 2 configuration specification
  367. #
  368. # @brief Append a piece of confspec
  369. #
  370. # @param orig dict : the confspec to update
  371. # @param section str : section name
  372. # @param key str
  373. # @param validator Validator : the validator to use to check this configuration key's value
  374. # @param default
  375. # @return new confspec
  376. #
  377. # @note orig is modified during the process
  378. def confspec_append(orig, section, key, validator, default):
  379. if section not in orig:
  380. orig[section] = dict()
  381. if key not in orig[section]:
  382. orig[section][key] = (default, validator)
  383. return orig
  384. ##
  385. # @brief Global specifications for lodel2 settings
  386. LODEL2_CONF_SPECS = {
  387. 'lodel2': {
  388. 'debug': (True, Validator('bool')),
  389. 'sitename': ('noname', Validator('strip')),
  390. 'runtest': (False, Validator('bool')),
  391. },
  392. 'lodel2.logging.*': {
  393. 'level': ('ERROR', Validator('loglevel')),
  394. 'context': (False, Validator('bool')),
  395. 'filename': ("-", Validator('errfile', none_is_valid=False)),
  396. 'backupcount': (5, Validator('int', none_is_valid=False)),
  397. 'maxbytes': (1024 * 10, Validator('int', none_is_valid=False)),
  398. },
  399. 'lodel2.editorialmodel': {
  400. 'emfile': ('em.pickle', Validator('strip')),
  401. 'emtranslator': ('picklefile', Validator('strip')),
  402. 'dyncode': ('leapi_dyncode.py', Validator('strip')),
  403. 'groups': ('', Validator('list')),
  404. 'editormode': (False, Validator('bool')),
  405. },
  406. 'lodel2.datasources.*': {
  407. 'read_only': (False, Validator('bool')),
  408. 'identifier': (None, Validator('string')),
  409. },
  410. 'lodel2.auth': {
  411. 'login_classfield': ('user.login', Validator('emfield')),
  412. 'pass_classfield': ('user.password', Validator('emfield')),
  413. },
  414. }