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

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