config.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. # -*- coding: utf-8 -*-
  2. from urllib.parse import urlparse
  3. import os
  4. from paste.deploy.converters import asbool
  5. from tracim_backend.lib.utils.logger import logger
  6. from depot.manager import DepotManager
  7. from tracim_backend.models.contents import CONTENT_TYPES
  8. from tracim_backend.models.data import ActionDescription
  9. class CFG(object):
  10. """Object used for easy access to config file parameters."""
  11. def __setattr__(self, key, value):
  12. """
  13. Log-ready setter.
  14. Logs all configuration parameters except password.
  15. :param key:
  16. :param value:
  17. :return:
  18. """
  19. if 'PASSWORD' not in key and \
  20. ('URL' not in key or type(value) == str) and \
  21. 'CONTENT' not in key:
  22. # We do not show PASSWORD for security reason
  23. # we do not show URL because At the time of configuration setup,
  24. # it can't be evaluated
  25. # We do not show CONTENT in order not to pollute log files
  26. logger.info(self, 'CONFIG: [ {} | {} ]'.format(key, value))
  27. else:
  28. logger.info(self, 'CONFIG: [ {} | <value not shown> ]'.format(key))
  29. self.__dict__[key] = value
  30. def __init__(self, settings):
  31. """Parse configuration file."""
  32. ###
  33. # General
  34. ###
  35. mandatory_msg = \
  36. 'ERROR: {} configuration is mandatory. Set it before continuing.'
  37. self.DEPOT_STORAGE_DIR = settings.get(
  38. 'depot_storage_dir',
  39. )
  40. if not self.DEPOT_STORAGE_DIR:
  41. raise Exception(
  42. mandatory_msg.format('depot_storage_dir')
  43. )
  44. self.DEPOT_STORAGE_NAME = settings.get(
  45. 'depot_storage_name',
  46. )
  47. if not self.DEPOT_STORAGE_NAME:
  48. raise Exception(
  49. mandatory_msg.format('depot_storage_name')
  50. )
  51. self.PREVIEW_CACHE_DIR = settings.get(
  52. 'preview_cache_dir',
  53. )
  54. if not self.PREVIEW_CACHE_DIR:
  55. raise Exception(
  56. 'ERROR: preview_cache_dir configuration is mandatory. '
  57. 'Set it before continuing.'
  58. )
  59. self.DATA_UPDATE_ALLOWED_DURATION = int(settings.get(
  60. 'content.update.allowed.duration',
  61. 0,
  62. ))
  63. self.WEBSITE_TITLE = settings.get(
  64. 'website.title',
  65. 'TRACIM',
  66. )
  67. self.WEBSITE_BASE_URL = settings.get(
  68. 'website.base_url',
  69. '',
  70. )
  71. # TODO - G.M - 26-03-2018 - [Cleanup] These params seems deprecated for tracimv2, # nopep8
  72. # Verify this
  73. #
  74. # self.WEBSITE_HOME_TITLE_COLOR = settings.get(
  75. # 'website.title.color',
  76. # '#555',
  77. # )
  78. # self.WEBSITE_HOME_IMAGE_PATH = settings.get(
  79. # '/assets/img/home_illustration.jpg',
  80. # )
  81. # self.WEBSITE_HOME_BACKGROUND_IMAGE_PATH = settings.get(
  82. # '/assets/img/bg.jpg',
  83. # )
  84. #
  85. self.WEBSITE_SERVER_NAME = settings.get(
  86. 'website.server_name',
  87. None,
  88. )
  89. if not self.WEBSITE_SERVER_NAME:
  90. self.WEBSITE_SERVER_NAME = urlparse(self.WEBSITE_BASE_URL).hostname
  91. logger.warning(
  92. self,
  93. 'NOTE: Generated website.server_name parameter from '
  94. 'website.base_url parameter -> {0}'
  95. .format(self.WEBSITE_SERVER_NAME)
  96. )
  97. self.WEBSITE_HOME_TAG_LINE = settings.get(
  98. 'website.home.tag_line',
  99. '',
  100. )
  101. self.WEBSITE_SUBTITLE = settings.get(
  102. 'website.home.subtitle',
  103. '',
  104. )
  105. self.WEBSITE_HOME_BELOW_LOGIN_FORM = settings.get(
  106. 'website.home.below_login_form',
  107. '',
  108. )
  109. self.WEBSITE_TREEVIEW_CONTENT = settings.get(
  110. 'website.treeview.content',
  111. )
  112. self.USER_AUTH_TOKEN_VALIDITY = int(settings.get(
  113. 'user.auth_token.validity',
  114. '604800',
  115. ))
  116. self.DEBUG = asbool(settings.get('debug', False))
  117. # TODO - G.M - 27-03-2018 - [Email] Restore email config
  118. ###
  119. # EMAIL related stuff (notification, reply)
  120. ##
  121. self.EMAIL_NOTIFICATION_NOTIFIED_EVENTS = [
  122. ActionDescription.COMMENT,
  123. ActionDescription.CREATION,
  124. ActionDescription.EDITION,
  125. ActionDescription.REVISION,
  126. ActionDescription.STATUS_UPDATE
  127. ]
  128. self.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS = [
  129. CONTENT_TYPES.Page.slug,
  130. CONTENT_TYPES.Thread.slug,
  131. CONTENT_TYPES.File.slug,
  132. CONTENT_TYPES.Comment.slug,
  133. # CONTENT_TYPES.Folder.slug -- Folder is skipped
  134. ]
  135. if settings.get('email.notification.from'):
  136. raise Exception(
  137. 'email.notification.from configuration is deprecated. '
  138. 'Use instead email.notification.from.email and '
  139. 'email.notification.from.default_label.'
  140. )
  141. self.EMAIL_NOTIFICATION_FROM_EMAIL = settings.get(
  142. 'email.notification.from.email',
  143. 'noreply+{user_id}@trac.im'
  144. )
  145. self.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL = settings.get(
  146. 'email.notification.from.default_label',
  147. 'Tracim Notifications'
  148. )
  149. self.EMAIL_NOTIFICATION_REPLY_TO_EMAIL = settings.get(
  150. 'email.notification.reply_to.email',
  151. )
  152. self.EMAIL_NOTIFICATION_REFERENCES_EMAIL = settings.get(
  153. 'email.notification.references.email'
  154. )
  155. self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML = settings.get(
  156. 'email.notification.content_update.template.html',
  157. )
  158. self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT = settings.get(
  159. 'email.notification.content_update.template.text',
  160. )
  161. self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML = settings.get(
  162. 'email.notification.created_account.template.html',
  163. './tracim_backend/templates/mail/created_account_body_html.mak',
  164. )
  165. self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT = settings.get(
  166. 'email.notification.created_account.template.text',
  167. './tracim_backend/templates/mail/created_account_body_text.mak',
  168. )
  169. self.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT = settings.get(
  170. 'email.notification.content_update.subject',
  171. )
  172. self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_SUBJECT = settings.get(
  173. 'email.notification.created_account.subject',
  174. '[{website_title}] Created account',
  175. )
  176. self.EMAIL_NOTIFICATION_PROCESSING_MODE = settings.get(
  177. 'email.notification.processing_mode',
  178. )
  179. self.EMAIL_NOTIFICATION_ACTIVATED = asbool(settings.get(
  180. 'email.notification.activated',
  181. ))
  182. self.EMAIL_NOTIFICATION_SMTP_SERVER = settings.get(
  183. 'email.notification.smtp.server',
  184. )
  185. self.EMAIL_NOTIFICATION_SMTP_PORT = settings.get(
  186. 'email.notification.smtp.port',
  187. )
  188. self.EMAIL_NOTIFICATION_SMTP_USER = settings.get(
  189. 'email.notification.smtp.user',
  190. )
  191. self.EMAIL_NOTIFICATION_SMTP_PASSWORD = settings.get(
  192. 'email.notification.smtp.password',
  193. )
  194. self.EMAIL_NOTIFICATION_LOG_FILE_PATH = settings.get(
  195. 'email.notification.log_file_path',
  196. None,
  197. )
  198. # self.EMAIL_REPLY_ACTIVATED = asbool(settings.get(
  199. # 'email.reply.activated',
  200. # False,
  201. # ))
  202. #
  203. # self.EMAIL_REPLY_IMAP_SERVER = settings.get(
  204. # 'email.reply.imap.server',
  205. # )
  206. # self.EMAIL_REPLY_IMAP_PORT = settings.get(
  207. # 'email.reply.imap.port',
  208. # )
  209. # self.EMAIL_REPLY_IMAP_USER = settings.get(
  210. # 'email.reply.imap.user',
  211. # )
  212. # self.EMAIL_REPLY_IMAP_PASSWORD = settings.get(
  213. # 'email.reply.imap.password',
  214. # )
  215. # self.EMAIL_REPLY_IMAP_FOLDER = settings.get(
  216. # 'email.reply.imap.folder',
  217. # )
  218. # self.EMAIL_REPLY_CHECK_HEARTBEAT = int(settings.get(
  219. # 'email.reply.check.heartbeat',
  220. # 60,
  221. # ))
  222. # self.EMAIL_REPLY_TOKEN = settings.get(
  223. # 'email.reply.token',
  224. # )
  225. # self.EMAIL_REPLY_IMAP_USE_SSL = asbool(settings.get(
  226. # 'email.reply.imap.use_ssl',
  227. # ))
  228. # self.EMAIL_REPLY_IMAP_USE_IDLE = asbool(settings.get(
  229. # 'email.reply.imap.use_idle',
  230. # True,
  231. # ))
  232. # self.EMAIL_REPLY_CONNECTION_MAX_LIFETIME = int(settings.get(
  233. # 'email.reply.connection.max_lifetime',
  234. # 600, # 10 minutes
  235. # ))
  236. # self.EMAIL_REPLY_USE_HTML_PARSING = asbool(settings.get(
  237. # 'email.reply.use_html_parsing',
  238. # True,
  239. # ))
  240. # self.EMAIL_REPLY_USE_TXT_PARSING = asbool(settings.get(
  241. # 'email.reply.use_txt_parsing',
  242. # True,
  243. # ))
  244. # self.EMAIL_REPLY_LOCKFILE_PATH = settings.get(
  245. # 'email.reply.lockfile_path',
  246. # ''
  247. # )
  248. # if not self.EMAIL_REPLY_LOCKFILE_PATH and self.EMAIL_REPLY_ACTIVATED:
  249. # raise Exception(
  250. # mandatory_msg.format('email.reply.lockfile_path')
  251. # )
  252. #
  253. self.EMAIL_PROCESSING_MODE = settings.get(
  254. 'email.processing_mode',
  255. 'sync',
  256. ).upper()
  257. if self.EMAIL_PROCESSING_MODE not in (
  258. self.CST.ASYNC,
  259. self.CST.SYNC,
  260. ):
  261. raise Exception(
  262. 'email.processing_mode '
  263. 'can ''be "{}" or "{}", not "{}"'.format(
  264. self.CST.ASYNC,
  265. self.CST.SYNC,
  266. self.EMAIL_PROCESSING_MODE,
  267. )
  268. )
  269. self.EMAIL_SENDER_REDIS_HOST = settings.get(
  270. 'email.async.redis.host',
  271. 'localhost',
  272. )
  273. self.EMAIL_SENDER_REDIS_PORT = int(settings.get(
  274. 'email.async.redis.port',
  275. 6379,
  276. ))
  277. self.EMAIL_SENDER_REDIS_DB = int(settings.get(
  278. 'email.async.redis.db',
  279. 0,
  280. ))
  281. ###
  282. # WSGIDAV (Webdav server)
  283. ###
  284. # TODO - G.M - 27-03-2018 - [WebDav] Restore wsgidav config
  285. #self.WSGIDAV_CONFIG_PATH = settings.get(
  286. # 'wsgidav.config_path',
  287. # 'wsgidav.conf',
  288. #)
  289. # TODO: Convert to importlib
  290. # http://stackoverflow.com/questions/41063938/use-importlib-instead-imp-for-non-py-file
  291. #self.wsgidav_config = imp.load_source(
  292. # 'wsgidav_config',
  293. # self.WSGIDAV_CONFIG_PATH,
  294. #)
  295. # self.WSGIDAV_PORT = self.wsgidav_config.port
  296. # self.WSGIDAV_CLIENT_BASE_URL = settings.get(
  297. # 'wsgidav.client.base_url',
  298. # None,
  299. # )
  300. #
  301. # if not self.WSGIDAV_CLIENT_BASE_URL:
  302. # self.WSGIDAV_CLIENT_BASE_URL = \
  303. # '{0}:{1}'.format(
  304. # self.WEBSITE_SERVER_NAME,
  305. # self.WSGIDAV_PORT,
  306. # )
  307. # logger.warning(self,
  308. # 'NOTE: Generated wsgidav.client.base_url parameter with '
  309. # 'followings parameters: website.server_name and '
  310. # 'wsgidav.conf port'.format(
  311. # self.WSGIDAV_CLIENT_BASE_URL,
  312. # )
  313. # )
  314. #
  315. # if not self.WSGIDAV_CLIENT_BASE_URL.endswith('/'):
  316. # self.WSGIDAV_CLIENT_BASE_URL += '/'
  317. # TODO - G.M - 27-03-2018 - [Caldav] Restore radicale config
  318. ###
  319. # RADICALE (Caldav server)
  320. ###
  321. # self.RADICALE_SERVER_HOST = settings.get(
  322. # 'radicale.server.host',
  323. # '127.0.0.1',
  324. # )
  325. # self.RADICALE_SERVER_PORT = int(settings.get(
  326. # 'radicale.server.port',
  327. # 5232,
  328. # ))
  329. # # Note: Other parameters needed to work in SSL (cert file, etc)
  330. # self.RADICALE_SERVER_SSL = asbool(settings.get(
  331. # 'radicale.server.ssl',
  332. # False,
  333. # ))
  334. # self.RADICALE_SERVER_FILE_SYSTEM_FOLDER = settings.get(
  335. # 'radicale.server.filesystem.folder',
  336. # )
  337. # if not self.RADICALE_SERVER_FILE_SYSTEM_FOLDER:
  338. # raise Exception(
  339. # mandatory_msg.format('radicale.server.filesystem.folder')
  340. # )
  341. # self.RADICALE_SERVER_ALLOW_ORIGIN = settings.get(
  342. # 'radicale.server.allow_origin',
  343. # None,
  344. # )
  345. # if not self.RADICALE_SERVER_ALLOW_ORIGIN:
  346. # self.RADICALE_SERVER_ALLOW_ORIGIN = self.WEBSITE_BASE_URL
  347. # logger.warning(self,
  348. # 'NOTE: Generated radicale.server.allow_origin parameter with '
  349. # 'followings parameters: website.base_url ({0})'
  350. # .format(self.WEBSITE_BASE_URL)
  351. # )
  352. #
  353. # self.RADICALE_SERVER_REALM_MESSAGE = settings.get(
  354. # 'radicale.server.realm_message',
  355. # 'Tracim Calendar - Password Required',
  356. # )
  357. #
  358. # self.RADICALE_CLIENT_BASE_URL_HOST = settings.get(
  359. # 'radicale.client.base_url.host',
  360. # 'http://{}:{}'.format(
  361. # self.RADICALE_SERVER_HOST,
  362. # self.RADICALE_SERVER_PORT,
  363. # ),
  364. # )
  365. #
  366. # self.RADICALE_CLIENT_BASE_URL_PREFIX = settings.get(
  367. # 'radicale.client.base_url.prefix',
  368. # '/',
  369. # )
  370. # # Ensure finished by '/'
  371. # if '/' != self.RADICALE_CLIENT_BASE_URL_PREFIX[-1]:
  372. # self.RADICALE_CLIENT_BASE_URL_PREFIX += '/'
  373. # if '/' != self.RADICALE_CLIENT_BASE_URL_PREFIX[0]:
  374. # self.RADICALE_CLIENT_BASE_URL_PREFIX \
  375. # = '/' + self.RADICALE_CLIENT_BASE_URL_PREFIX
  376. #
  377. # if not self.RADICALE_CLIENT_BASE_URL_HOST:
  378. # logger.warning(self,
  379. # 'Generated radicale.client.base_url.host parameter with '
  380. # 'followings parameters: website.server_name -> {}'
  381. # .format(self.WEBSITE_SERVER_NAME)
  382. # )
  383. # self.RADICALE_CLIENT_BASE_URL_HOST = self.WEBSITE_SERVER_NAME
  384. #
  385. # self.RADICALE_CLIENT_BASE_URL_TEMPLATE = '{}{}'.format(
  386. # self.RADICALE_CLIENT_BASE_URL_HOST,
  387. # self.RADICALE_CLIENT_BASE_URL_PREFIX,
  388. # )
  389. self.PREVIEW_JPG_RESTRICTED_DIMS = asbool(settings.get(
  390. 'preview.jpg.restricted_dims', False
  391. ))
  392. preview_jpg_allowed_dims_str = settings.get('preview.jpg.allowed_dims', '') # nopep8
  393. allowed_dims = []
  394. if preview_jpg_allowed_dims_str:
  395. for sizes in preview_jpg_allowed_dims_str.split(','):
  396. parts = sizes.split('x')
  397. assert len(parts) == 2
  398. width, height = parts
  399. assert width.isdecimal()
  400. assert height.isdecimal()
  401. size = PreviewDim(int(width), int(height))
  402. allowed_dims.append(size)
  403. if not allowed_dims:
  404. size = PreviewDim(256, 256)
  405. allowed_dims.append(size)
  406. self.PREVIEW_JPG_ALLOWED_DIMS = allowed_dims
  407. self.FRONTEND_SERVE = asbool(settings.get(
  408. 'frontend.serve', False
  409. ))
  410. # INFO - G.M - 2018-08-06 - we pretend that frontend_dist_folder
  411. # is probably in frontend subfolder
  412. # of tracim_v2 parent of both backend and frontend
  413. backend_folder = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # nopep8
  414. tracim_v2_folder = os.path.dirname(backend_folder)
  415. frontend_dist_folder = os.path.join(tracim_v2_folder, 'frontend', 'dist') # nopep8
  416. self.FRONTEND_DIST_FOLDER_PATH = settings.get(
  417. 'frontend.dist_folder_path', frontend_dist_folder
  418. )
  419. # INFO - G.M - 2018-08-06 - We check dist folder existence
  420. if self.FRONTEND_SERVE and not os.path.isdir(self.FRONTEND_DIST_FOLDER_PATH): # nopep8
  421. raise Exception(
  422. 'ERROR: {} folder does not exist as folder. '
  423. 'please set frontend.dist_folder.path'
  424. 'with a correct value'.format(self.FRONTEND_DIST_FOLDER_PATH)
  425. )
  426. def configure_filedepot(self):
  427. depot_storage_name = self.DEPOT_STORAGE_NAME
  428. depot_storage_path = self.DEPOT_STORAGE_DIR
  429. depot_storage_settings = {'depot.storage_path': depot_storage_path}
  430. DepotManager.configure(
  431. depot_storage_name,
  432. depot_storage_settings,
  433. )
  434. class CST(object):
  435. ASYNC = 'ASYNC'
  436. SYNC = 'SYNC'
  437. TREEVIEW_FOLDERS = 'folders'
  438. TREEVIEW_ALL = 'all'
  439. class PreviewDim(object):
  440. def __init__(self, width: int, height: int) -> None:
  441. self.width = width
  442. self.height = height
  443. def __repr__(self):
  444. return "<PreviewDim width:{width} height:{height}>".format(
  445. width=self.width,
  446. height=self.height,
  447. )