app_cfg.py 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. # -*- coding: utf-8 -*-
  2. """
  3. Global configuration file for TG2-specific settings in tracim.
  4. This file complements development/deployment.ini.
  5. Please note that **all the argument values are strings**. If you want to
  6. convert them into boolean, for example, you should use the
  7. :func:`paste.deploy.converters.asbool` function, as in::
  8. from paste.deploy.converters import asbool
  9. setting = asbool(global_conf.get('the_setting'))
  10. """
  11. import imp
  12. import os
  13. from urllib.parse import urlparse
  14. from depot.manager import DepotManager
  15. from paste.deploy.converters import asbool
  16. import tg
  17. from tg.configuration.milestones import environment_loaded
  18. from tgext.pluggable import plug
  19. from tgext.pluggable import replace_template
  20. import tracim
  21. from tracim.config import TracimAppConfig
  22. from tracim.lib.base import logger
  23. from tracim.lib.daemons import DaemonsManager
  24. from tracim.lib.daemons import MailSenderDaemon
  25. from tracim.lib.daemons import MailFetcherDaemon
  26. from tracim.lib.daemons import RadicaleDaemon
  27. from tracim.lib.daemons import WsgiDavDaemon
  28. from tracim.lib.system import InterruptManager
  29. from tracim.lib.utils import lazy_ugettext as l_
  30. from tracim.model.data import ActionDescription
  31. from tracim.model.data import ContentType
  32. base_config = TracimAppConfig()
  33. base_config.renderers = []
  34. base_config.use_toscawidgets = False
  35. base_config.use_toscawidgets2 = True
  36. base_config.package = tracim
  37. # Enable json in expose
  38. base_config.renderers.append('json')
  39. # Enable genshi in expose to have a lingua franca for extensions and pluggable
  40. # apps
  41. # you can remove this if you don't plan to use it.
  42. base_config.renderers.append('genshi')
  43. # Set the default renderer
  44. base_config.default_renderer = 'mako'
  45. base_config.renderers.append('mako')
  46. # Configure the base SQLALchemy Setup
  47. base_config.use_sqlalchemy = True
  48. base_config.model = tracim.model
  49. base_config.DBSession = tracim.model.DBSession
  50. # This value can be modified by tracim.lib.auth.wrapper.AuthConfigWrapper but
  51. # have to be specified before
  52. base_config.auth_backend = 'sqlalchemy'
  53. # base_config.flash.cookie_name
  54. # base_config.flash.default_status -> Default message status if not specified
  55. # (ok by default)
  56. base_config['flash.template'] = '''
  57. <div class="alert alert-${status}" style="margin-top: 1em;">
  58. <button type="button" class="close" data-dismiss="alert">&times;</button>
  59. <div id="${container_id}">
  60. <img src="/assets/icons/32x32/status/flash-${status}.png"/>
  61. ${message}
  62. </div>
  63. </div>
  64. '''
  65. # -> string.Template instance used as the flash template when rendered from
  66. # server side, will receive $container_id, $message and $status variables.
  67. # flash.js_call -> javascript code which will be run when displaying the flash
  68. # from javascript. Default is webflash.render(), you can use webflash.payload()
  69. # to retrieve the message and show it with your favourite library.
  70. # flash.js_template -> string.Template instance used to replace full javascript
  71. # support for flash messages. When rendering flash message for javascript usage
  72. # the following code will be used instead of providing the standard webflash
  73. # object. If you replace js_template you must also ensure cookie parsing and
  74. # delete it for already displayed messages. The template will receive:
  75. # $container_id, $cookie_name, $js_call variables.
  76. base_config['templating.genshi.name_constant_patch'] = True
  77. # Configure the authentication backend
  78. # YOU MUST CHANGE THIS VALUE IN PRODUCTION TO SECURE YOUR APP
  79. base_config.sa_auth.cookie_secret = "3283411b-1904-4554-b0e1-883863b53080"
  80. # INFO - This is the way to specialize the resetpassword email properties
  81. # plug(base_config,
  82. # 'resetpassword',
  83. # None,
  84. # mail_subject=reset_password_email_subject)
  85. plug(base_config, 'resetpassword', 'reset_password')
  86. replace_template(base_config,
  87. 'resetpassword.templates.index',
  88. 'tracim.templates.reset_password_index')
  89. replace_template(base_config,
  90. 'resetpassword.templates.change_password',
  91. 'mako:tracim.templates.reset_password_change_password')
  92. daemons = DaemonsManager()
  93. def start_daemons(manager: DaemonsManager):
  94. """Start Tracim daemons."""
  95. from tg import config
  96. cfg = CFG.get_instance()
  97. # Don't start daemons if they are disabled
  98. if config.get('disable_daemons', False):
  99. return
  100. manager.run('radicale', RadicaleDaemon)
  101. manager.run('webdav', WsgiDavDaemon)
  102. if cfg.EMAIL_PROCESSING_MODE == CFG.CST.ASYNC:
  103. manager.run('mail_sender', MailSenderDaemon)
  104. if cfg.EMAIL_REPLY_ACTIVATED:
  105. manager.run('mail_fetcher', MailFetcherDaemon)
  106. def configure_depot():
  107. """Configure Depot."""
  108. depot_storage_name = CFG.get_instance().DEPOT_STORAGE_NAME
  109. depot_storage_path = CFG.get_instance().DEPOT_STORAGE_DIR
  110. depot_storage_settings = {'depot.storage_path': depot_storage_path}
  111. DepotManager.configure(
  112. depot_storage_name,
  113. depot_storage_settings,
  114. )
  115. environment_loaded.register(lambda: start_daemons(daemons))
  116. environment_loaded.register(lambda: configure_depot())
  117. interrupt_manager = InterruptManager(os.getpid(), daemons_manager=daemons)
  118. #######
  119. #
  120. # INFO - D.A. - 2014-10-31
  121. # fake strings allowing to translate resetpassword tgapp.
  122. # TODO - Integrate these translations into tgapp-resetpassword
  123. #
  124. l_('New password')
  125. l_('Confirm new password')
  126. l_('Save new password')
  127. l_('Email address')
  128. l_('Send Request')
  129. l_('Password reset request')
  130. l_('Password reset request sent')
  131. l_('Invalid password reset request')
  132. l_('Password reset request timed out')
  133. l_('Invalid password reset request')
  134. l_('Password changed successfully')
  135. l_('''
  136. We've received a request to reset the password for this account.
  137. Please click this link to reset your password:
  138. %(password_reset_link)s
  139. If you no longer wish to make the above change, or if you did not initiate this request, please disregard and/or delete this e-mail.
  140. ''')
  141. class CFG(object):
  142. """Singleton used for easy access to config file parameters."""
  143. _instance = None
  144. @classmethod
  145. def get_instance(cls) -> 'CFG':
  146. """Get configuration singleton."""
  147. if not CFG._instance:
  148. CFG._instance = CFG()
  149. return CFG._instance
  150. def __setattr__(self, key, value):
  151. """
  152. Log-ready setter.
  153. Logs all configuration parameters except password.
  154. :param key:
  155. :param value:
  156. :return:
  157. """
  158. if 'PASSWORD' not in key and \
  159. ('URL' not in key or type(value) == str) and \
  160. 'CONTENT' not in key:
  161. # We do not show PASSWORD for security reason
  162. # we do not show URL because the associated config uses tg.lurl()
  163. # which is evaluated when at display time.
  164. # At the time of configuration setup, it can't be evaluated
  165. # We do not show CONTENT in order not to pollute log files
  166. logger.info(self, 'CONFIG: [ {} | {} ]'.format(key, value))
  167. else:
  168. logger.info(self, 'CONFIG: [ {} | <value not shown> ]'.format(key))
  169. self.__dict__[key] = value
  170. def __init__(self):
  171. """Parse configuration file."""
  172. mandatory_msg = \
  173. 'ERROR: {} configuration is mandatory. Set it before continuing.'
  174. self.DEPOT_STORAGE_DIR = tg.config.get(
  175. 'depot_storage_dir',
  176. )
  177. if not self.DEPOT_STORAGE_DIR:
  178. raise Exception(
  179. mandatory_msg.format('depot_storage_dir')
  180. )
  181. self.DEPOT_STORAGE_NAME = tg.config.get(
  182. 'depot_storage_name',
  183. )
  184. if not self.DEPOT_STORAGE_NAME:
  185. raise Exception(
  186. mandatory_msg.format('depot_storage_name')
  187. )
  188. self.PREVIEW_CACHE_DIR = tg.config.get(
  189. 'preview_cache_dir',
  190. )
  191. if not self.PREVIEW_CACHE_DIR:
  192. raise Exception(
  193. 'ERROR: preview_cache_dir configuration is mandatory. '
  194. 'Set it before continuing.'
  195. )
  196. self.DATA_UPDATE_ALLOWED_DURATION = int(tg.config.get(
  197. 'content.update.allowed.duration',
  198. 0,
  199. ))
  200. self.WEBSITE_TITLE = tg.config.get(
  201. 'website.title',
  202. 'TRACIM',
  203. )
  204. self.WEBSITE_HOME_TITLE_COLOR = tg.config.get(
  205. 'website.title.color',
  206. '#555',
  207. )
  208. self.WEBSITE_HOME_IMAGE_URL = tg.lurl(
  209. '/assets/img/home_illustration.jpg',
  210. )
  211. self.WEBSITE_HOME_BACKGROUND_IMAGE_URL = tg.lurl(
  212. '/assets/img/bg.jpg',
  213. )
  214. self.WEBSITE_BASE_URL = tg.config.get(
  215. 'website.base_url',
  216. '',
  217. )
  218. self.WEBSITE_SERVER_NAME = tg.config.get(
  219. 'website.server_name',
  220. None,
  221. )
  222. if not self.WEBSITE_SERVER_NAME:
  223. self.WEBSITE_SERVER_NAME = urlparse(self.WEBSITE_BASE_URL).hostname
  224. logger.warning(
  225. self,
  226. 'NOTE: Generated website.server_name parameter from '
  227. 'website.base_url parameter -> {0}'
  228. .format(self.WEBSITE_SERVER_NAME)
  229. )
  230. self.WEBSITE_HOME_TAG_LINE = tg.config.get(
  231. 'website.home.tag_line',
  232. '',
  233. )
  234. self.WEBSITE_SUBTITLE = tg.config.get(
  235. 'website.home.subtitle',
  236. '',
  237. )
  238. self.WEBSITE_HOME_BELOW_LOGIN_FORM = tg.config.get(
  239. 'website.home.below_login_form',
  240. '',
  241. )
  242. if tg.config.get('email.notification.from'):
  243. raise Exception(
  244. 'email.notification.from configuration is deprecated. '
  245. 'Use instead email.notification.from.email and '
  246. 'email.notification.from.default_label.'
  247. )
  248. self.EMAIL_NOTIFICATION_FROM_EMAIL = tg.config.get(
  249. 'email.notification.from.email',
  250. )
  251. self.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL = tg.config.get(
  252. 'email.notification.from.default_label'
  253. )
  254. self.EMAIL_NOTIFICATION_REPLY_TO_EMAIL = tg.config.get(
  255. 'email.notification.reply_to.email',
  256. )
  257. self.EMAIL_NOTIFICATION_REFERENCES_EMAIL = tg.config.get(
  258. 'email.notification.references.email'
  259. )
  260. self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML = tg.config.get(
  261. 'email.notification.content_update.template.html',
  262. )
  263. self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT = tg.config.get(
  264. 'email.notification.content_update.template.text',
  265. )
  266. self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML = tg.config.get(
  267. 'email.notification.created_account.template.html',
  268. './tracim/templates/mail/created_account_body_html.mak',
  269. )
  270. self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT = tg.config.get(
  271. 'email.notification.created_account.template.text',
  272. './tracim/templates/mail/created_account_body_text.mak',
  273. )
  274. self.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT = tg.config.get(
  275. 'email.notification.content_update.subject',
  276. )
  277. self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_SUBJECT = tg.config.get(
  278. 'email.notification.created_account.subject',
  279. '[{website_title}] Created account',
  280. )
  281. self.EMAIL_NOTIFICATION_PROCESSING_MODE = tg.config.get(
  282. 'email.notification.processing_mode',
  283. )
  284. self.EMAIL_NOTIFICATION_ACTIVATED = asbool(tg.config.get(
  285. 'email.notification.activated',
  286. ))
  287. self.EMAIL_NOTIFICATION_SMTP_SERVER = tg.config.get(
  288. 'email.notification.smtp.server',
  289. )
  290. self.EMAIL_NOTIFICATION_SMTP_PORT = tg.config.get(
  291. 'email.notification.smtp.port',
  292. )
  293. self.EMAIL_NOTIFICATION_SMTP_USER = tg.config.get(
  294. 'email.notification.smtp.user',
  295. )
  296. self.EMAIL_NOTIFICATION_SMTP_PASSWORD = tg.config.get(
  297. 'email.notification.smtp.password',
  298. )
  299. self.EMAIL_NOTIFICATION_LOG_FILE_PATH = tg.config.get(
  300. 'email.notification.log_file_path',
  301. None,
  302. )
  303. self.EMAIL_REPLY_ACTIVATED = asbool(tg.config.get(
  304. 'email.reply.activated',
  305. False,
  306. ))
  307. self.EMAIL_REPLY_IMAP_SERVER = tg.config.get(
  308. 'email.reply.imap.server',
  309. )
  310. self.EMAIL_REPLY_IMAP_PORT = tg.config.get(
  311. 'email.reply.imap.port',
  312. )
  313. self.EMAIL_REPLY_IMAP_USER = tg.config.get(
  314. 'email.reply.imap.user',
  315. )
  316. self.EMAIL_REPLY_IMAP_PASSWORD = tg.config.get(
  317. 'email.reply.imap.password',
  318. )
  319. self.EMAIL_REPLY_IMAP_FOLDER = tg.config.get(
  320. 'email.reply.imap.folder',
  321. )
  322. self.EMAIL_REPLY_CHECK_HEARTBEAT = int(tg.config.get(
  323. 'email.reply.check.heartbeat',
  324. 60,
  325. ))
  326. self.EMAIL_REPLY_TOKEN = tg.config.get(
  327. 'email.reply.token',
  328. )
  329. self.EMAIL_REPLY_IMAP_USE_SSL = asbool(tg.config.get(
  330. 'email.reply.imap.use_ssl',
  331. ))
  332. self.EMAIL_REPLY_USE_HTML_PARSING = asbool(tg.config.get(
  333. 'email.reply.use_html_parsing',
  334. True,
  335. ))
  336. self.EMAIL_REPLY_USE_TXT_PARSING = asbool(tg.config.get(
  337. 'email.reply.use_txt_parsing',
  338. True,
  339. ))
  340. self.EMAIL_REPLY_FILELOCK_PATH = tg.config.get(
  341. 'email.reply.filelock_path',
  342. )
  343. self.TRACKER_JS_PATH = tg.config.get(
  344. 'js_tracker_path',
  345. )
  346. self.TRACKER_JS_CONTENT = self.get_tracker_js_content(
  347. self.TRACKER_JS_PATH,
  348. )
  349. self.WEBSITE_TREEVIEW_CONTENT = tg.config.get(
  350. 'website.treeview.content',
  351. )
  352. self.EMAIL_NOTIFICATION_NOTIFIED_EVENTS = [
  353. ActionDescription.COMMENT,
  354. ActionDescription.CREATION,
  355. ActionDescription.EDITION,
  356. ActionDescription.REVISION,
  357. ActionDescription.STATUS_UPDATE
  358. ]
  359. self.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS = [
  360. ContentType.Page,
  361. ContentType.Thread,
  362. ContentType.File,
  363. ContentType.Comment,
  364. # ContentType.Folder -- Folder is skipped
  365. ]
  366. self.RADICALE_SERVER_HOST = tg.config.get(
  367. 'radicale.server.host',
  368. '127.0.0.1',
  369. )
  370. self.RADICALE_SERVER_PORT = int(tg.config.get(
  371. 'radicale.server.port',
  372. 5232,
  373. ))
  374. # Note: Other parameters needed to work in SSL (cert file, etc)
  375. self.RADICALE_SERVER_SSL = asbool(tg.config.get(
  376. 'radicale.server.ssl',
  377. False,
  378. ))
  379. self.RADICALE_SERVER_FILE_SYSTEM_FOLDER = tg.config.get(
  380. 'radicale.server.filesystem.folder',
  381. )
  382. if not self.RADICALE_SERVER_FILE_SYSTEM_FOLDER:
  383. raise Exception(
  384. mandatory_msg.format('radicale.server.filesystem.folder')
  385. )
  386. self.RADICALE_SERVER_ALLOW_ORIGIN = tg.config.get(
  387. 'radicale.server.allow_origin',
  388. None,
  389. )
  390. if not self.RADICALE_SERVER_ALLOW_ORIGIN:
  391. self.RADICALE_SERVER_ALLOW_ORIGIN = self.WEBSITE_BASE_URL
  392. logger.warning(
  393. self,
  394. 'NOTE: Generated radicale.server.allow_origin parameter with '
  395. 'followings parameters: website.base_url ({0})'
  396. .format(self.WEBSITE_BASE_URL)
  397. )
  398. self.RADICALE_SERVER_REALM_MESSAGE = tg.config.get(
  399. 'radicale.server.realm_message',
  400. 'Tracim Calendar - Password Required',
  401. )
  402. self.RADICALE_CLIENT_BASE_URL_HOST = tg.config.get(
  403. 'radicale.client.base_url.host',
  404. 'http://{}:{}'.format(
  405. self.RADICALE_SERVER_HOST,
  406. self.RADICALE_SERVER_PORT,
  407. ),
  408. )
  409. self.RADICALE_CLIENT_BASE_URL_PREFIX = tg.config.get(
  410. 'radicale.client.base_url.prefix',
  411. '/',
  412. )
  413. # Ensure finished by '/'
  414. if '/' != self.RADICALE_CLIENT_BASE_URL_PREFIX[-1]:
  415. self.RADICALE_CLIENT_BASE_URL_PREFIX += '/'
  416. if '/' != self.RADICALE_CLIENT_BASE_URL_PREFIX[0]:
  417. self.RADICALE_CLIENT_BASE_URL_PREFIX \
  418. = '/' + self.RADICALE_CLIENT_BASE_URL_PREFIX
  419. if not self.RADICALE_CLIENT_BASE_URL_HOST:
  420. logger.warning(
  421. self,
  422. 'Generated radicale.client.base_url.host parameter with '
  423. 'followings parameters: website.server_name -> {}'
  424. .format(self.WEBSITE_SERVER_NAME)
  425. )
  426. self.RADICALE_CLIENT_BASE_URL_HOST = self.WEBSITE_SERVER_NAME
  427. self.RADICALE_CLIENT_BASE_URL_TEMPLATE = '{}{}'.format(
  428. self.RADICALE_CLIENT_BASE_URL_HOST,
  429. self.RADICALE_CLIENT_BASE_URL_PREFIX,
  430. )
  431. self.USER_AUTH_TOKEN_VALIDITY = int(tg.config.get(
  432. 'user.auth_token.validity',
  433. '604800',
  434. ))
  435. self.WSGIDAV_CONFIG_PATH = tg.config.get(
  436. 'wsgidav.config_path',
  437. 'wsgidav.conf',
  438. )
  439. # TODO: Convert to importlib
  440. # http://stackoverflow.com/questions/41063938/use-importlib-instead-imp-for-non-py-file
  441. self.wsgidav_config = imp.load_source(
  442. 'wsgidav_config',
  443. self.WSGIDAV_CONFIG_PATH,
  444. )
  445. self.WSGIDAV_PORT = self.wsgidav_config.port
  446. self.WSGIDAV_CLIENT_BASE_URL = tg.config.get(
  447. 'wsgidav.client.base_url',
  448. None,
  449. )
  450. if not self.WSGIDAV_CLIENT_BASE_URL:
  451. self.WSGIDAV_CLIENT_BASE_URL = \
  452. '{0}:{1}'.format(
  453. self.WEBSITE_SERVER_NAME,
  454. self.WSGIDAV_PORT,
  455. )
  456. logger.warning(
  457. self,
  458. 'NOTE: Generated wsgidav.client.base_url parameter with '
  459. 'followings parameters: website.server_name and '
  460. 'wsgidav.conf port'.format(
  461. self.WSGIDAV_CLIENT_BASE_URL,
  462. )
  463. )
  464. if not self.WSGIDAV_CLIENT_BASE_URL.endswith('/'):
  465. self.WSGIDAV_CLIENT_BASE_URL += '/'
  466. self.EMAIL_PROCESSING_MODE = tg.config.get(
  467. 'email.processing_mode',
  468. 'sync',
  469. ).upper()
  470. if self.EMAIL_PROCESSING_MODE not in (
  471. self.CST.ASYNC,
  472. self.CST.SYNC,
  473. ):
  474. raise Exception(
  475. 'email.processing_mode '
  476. 'can ''be "{}" or "{}", not "{}"'.format(
  477. self.CST.ASYNC,
  478. self.CST.SYNC,
  479. self.EMAIL_PROCESSING_MODE,
  480. )
  481. )
  482. self.EMAIL_SENDER_REDIS_HOST = tg.config.get(
  483. 'email.async.redis.host',
  484. 'localhost',
  485. )
  486. self.EMAIL_SENDER_REDIS_PORT = int(tg.config.get(
  487. 'email.async.redis.port',
  488. 6379,
  489. ))
  490. self.EMAIL_SENDER_REDIS_DB = int(tg.config.get(
  491. 'email.async.redis.db',
  492. 0,
  493. ))
  494. def get_tracker_js_content(self, js_tracker_file_path=None):
  495. """Get frontend analytics file."""
  496. result = ''
  497. js_tracker_file_path = tg.config.get('js_tracker_path', None)
  498. if js_tracker_file_path:
  499. info_log = 'Reading JS tracking code from file {}'
  500. logger.info(self, info_log.format(js_tracker_file_path))
  501. with open(js_tracker_file_path, 'r') as js_file:
  502. data = js_file.read()
  503. result = data
  504. return result
  505. class CST(object):
  506. ASYNC = 'ASYNC'
  507. SYNC = 'SYNC'
  508. TREEVIEW_FOLDERS = 'folders'
  509. TREEVIEW_ALL = 'all'
  510. #######
  511. #
  512. # INFO - D.A. - 2014-11-05
  513. # Allow to process asynchronous tasks
  514. # This is used for email notifications
  515. #
  516. # import tgext.asyncjob
  517. # tgext.asyncjob.plugme(base_config)
  518. #
  519. # OR
  520. #
  521. # plug(base_config, 'tgext.asyncjob', app_globals=base_config)
  522. #
  523. # OR
  524. #
  525. # Add some variable to each templates
  526. base_config.variable_provider = lambda: {
  527. 'CFG': CFG.get_instance()
  528. }