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

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