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

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