helpers.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.utils.helpers
  4. ~~~~~~~~~~~~~~~~~~~~~
  5. A few helpers that are used by flaskbb
  6. :copyright: (c) 2014 by the FlaskBB Team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. import ast
  10. import itertools
  11. import logging
  12. import operator
  13. import warnings
  14. import os
  15. import re
  16. import time
  17. from datetime import datetime, timedelta
  18. from email import message_from_string
  19. from functools import wraps
  20. from pkg_resources import get_distribution
  21. import requests
  22. import unidecode
  23. from babel.core import get_locale_identifier
  24. from babel.dates import (
  25. format_timedelta as babel_format_timedelta,
  26. format_datetime as babel_format_datetime,
  27. format_date as babel_format_date)
  28. from werkzeug.utils import import_string, ImportStringError
  29. from flask import flash, g, redirect, request, session, url_for
  30. from flask_allows import Permission
  31. from flask_babelplus import lazy_gettext as _
  32. from flask_login import current_user
  33. from flask_themes2 import get_themes_list, render_theme_template
  34. from flaskbb._compat import (iteritems, range_method, text_type, string_types,
  35. to_bytes, to_unicode)
  36. from flaskbb.extensions import babel, redis_store
  37. from flaskbb.utils.settings import flaskbb_config
  38. from jinja2 import Markup
  39. from PIL import ImageFile
  40. from pytz import UTC
  41. from werkzeug.local import LocalProxy
  42. try: # compat
  43. FileNotFoundError
  44. except NameError:
  45. FileNotFoundError = IOError
  46. logger = logging.getLogger(__name__)
  47. _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
  48. def slugify(text, delim=u'-'):
  49. """Generates an slightly worse ASCII-only slug.
  50. Taken from the Flask Snippets page.
  51. :param text: The text which should be slugified
  52. :param delim: Default "-". The delimeter for whitespace
  53. """
  54. text = unidecode.unidecode(text)
  55. result = []
  56. for word in _punct_re.split(text.lower()):
  57. if word:
  58. result.append(word)
  59. return text_type(delim.join(result))
  60. def redirect_or_next(endpoint, **kwargs):
  61. """Redirects the user back to the page they were viewing or to a specified
  62. endpoint. Wraps Flasks :func:`Flask.redirect` function.
  63. :param endpoint: The fallback endpoint.
  64. """
  65. return redirect(
  66. request.args.get('next') or endpoint, **kwargs
  67. )
  68. def render_template(template, **context): # pragma: no cover
  69. """A helper function that uses the `render_theme_template` function
  70. without needing to edit all the views
  71. """
  72. if current_user.is_authenticated and current_user.theme:
  73. theme = current_user.theme
  74. else:
  75. theme = session.get('theme', flaskbb_config['DEFAULT_THEME'])
  76. return render_theme_template(theme, template, **context)
  77. def do_topic_action(topics, user, action, reverse):
  78. """Executes a specific action for topics. Returns a list with the modified
  79. topic objects.
  80. :param topics: A iterable with ``Topic`` objects.
  81. :param user: The user object which wants to perform the action.
  82. :param action: One of the following actions: locked, important and delete.
  83. :param reverse: If the action should be done in a reversed way.
  84. For example, to unlock a topic, ``reverse`` should be
  85. set to ``True``.
  86. """
  87. if not topics:
  88. return False
  89. from flaskbb.utils.requirements import (IsAtleastModeratorInForum,
  90. CanDeleteTopic, Has)
  91. if not Permission(IsAtleastModeratorInForum(forum=topics[0].forum)):
  92. flash(_("You do not have the permissions to execute this "
  93. "action."), "danger")
  94. return False
  95. modified_topics = 0
  96. if action not in {'delete', 'hide', 'unhide'}:
  97. for topic in topics:
  98. if getattr(topic, action) and not reverse:
  99. continue
  100. setattr(topic, action, not reverse)
  101. modified_topics += 1
  102. topic.save()
  103. elif action == "delete":
  104. if not Permission(CanDeleteTopic):
  105. flash(_("You do not have the permissions to delete these topics."), "danger")
  106. return False
  107. for topic in topics:
  108. modified_topics += 1
  109. topic.delete()
  110. elif action == 'hide':
  111. if not Permission(Has('makehidden')):
  112. flash(_("You do not have the permissions to hide these topics."), "danger")
  113. return False
  114. for topic in topics:
  115. if topic.hidden:
  116. continue
  117. modified_topics += 1
  118. topic.hide(user)
  119. elif action == 'unhide':
  120. if not Permission(Has('makehidden')):
  121. flash(_("You do not have the permissions to unhide these topics."), "danger")
  122. return False
  123. for topic in topics:
  124. if not topic.hidden:
  125. continue
  126. modified_topics += 1
  127. topic.unhide()
  128. return modified_topics
  129. def get_categories_and_forums(query_result, user):
  130. """Returns a list with categories. Every category has a list for all
  131. their associated forums.
  132. The structure looks like this::
  133. [(<Category 1>,
  134. [(<Forum 1>, None),
  135. (<Forum 2>, <flaskbb.forum.models.ForumsRead at 0x38fdb50>)]),
  136. (<Category 2>,
  137. [(<Forum 3>, None),
  138. (<Forum 4>, None)])]
  139. and to unpack the values you can do this::
  140. In [110]: for category, forums in x:
  141. .....: print category
  142. .....: for forum, forumsread in forums:
  143. .....: print "\t", forum, forumsread
  144. This will print something like this:
  145. <Category 1>
  146. <Forum 1> None
  147. <Forum 2> <flaskbb.forum.models.ForumsRead object at 0x38fdb50>
  148. <Category 2>
  149. <Forum 3> None
  150. <Forum 4> None
  151. :param query_result: A tuple (KeyedTuple) with all categories and forums
  152. :param user: The user object is needed because a signed out user does not
  153. have the ForumsRead relation joined.
  154. """
  155. it = itertools.groupby(query_result, operator.itemgetter(0))
  156. forums = []
  157. if user.is_authenticated:
  158. for key, value in it:
  159. forums.append((key, [(item[1], item[2]) for item in value]))
  160. else:
  161. for key, value in it:
  162. forums.append((key, [(item[1], None) for item in value]))
  163. return forums
  164. def get_forums(query_result, user):
  165. """Returns a tuple which contains the category and the forums as list.
  166. This is the counterpart for get_categories_and_forums and especially
  167. usefull when you just need the forums for one category.
  168. For example::
  169. (<Category 2>,
  170. [(<Forum 3>, None),
  171. (<Forum 4>, None)])
  172. :param query_result: A tuple (KeyedTuple) with all categories and forums
  173. :param user: The user object is needed because a signed out user does not
  174. have the ForumsRead relation joined.
  175. """
  176. it = itertools.groupby(query_result, operator.itemgetter(0))
  177. if user.is_authenticated:
  178. for key, value in it:
  179. forums = key, [(item[1], item[2]) for item in value]
  180. else:
  181. for key, value in it:
  182. forums = key, [(item[1], None) for item in value]
  183. return forums
  184. def forum_is_unread(forum, forumsread, user):
  185. """Checks if a forum is unread
  186. :param forum: The forum that should be checked if it is unread
  187. :param forumsread: The forumsread object for the forum
  188. :param user: The user who should be checked if he has read the forum
  189. """
  190. # If the user is not signed in, every forum is marked as read
  191. if not user.is_authenticated:
  192. return False
  193. read_cutoff = time_utcnow() - timedelta(
  194. days=flaskbb_config["TRACKER_LENGTH"])
  195. # disable tracker if TRACKER_LENGTH is set to 0
  196. if flaskbb_config["TRACKER_LENGTH"] == 0:
  197. return False
  198. # If there are no topics in the forum, mark it as read
  199. if forum and forum.topic_count == 0:
  200. return False
  201. # check if the last post is newer than the tracker length
  202. if forum.last_post_id is None or forum.last_post_created < read_cutoff:
  203. return False
  204. # If the user hasn't visited a topic in the forum - therefore,
  205. # forumsread is None and we need to check if it is still unread
  206. if forum and not forumsread:
  207. return forum.last_post_created > read_cutoff
  208. try:
  209. # check if the forum has been cleared and if there is a new post
  210. # since it have been cleared
  211. if forum.last_post_created > forumsread.cleared:
  212. if forum.last_post_created < forumsread.last_read:
  213. return False
  214. except TypeError:
  215. pass
  216. # else just check if the user has read the last post
  217. return forum.last_post_created > forumsread.last_read
  218. def topic_is_unread(topic, topicsread, user, forumsread=None):
  219. """Checks if a topic is unread.
  220. :param topic: The topic that should be checked if it is unread
  221. :param topicsread: The topicsread object for the topic
  222. :param user: The user who should be checked if he has read the last post
  223. in the topic
  224. :param forumsread: The forumsread object in which the topic is. If you
  225. also want to check if the user has marked the forum as
  226. read, than you will also need to pass an forumsread
  227. object.
  228. """
  229. if not user.is_authenticated:
  230. return False
  231. read_cutoff = time_utcnow() - timedelta(
  232. days=flaskbb_config["TRACKER_LENGTH"])
  233. # disable tracker if read_cutoff is set to 0
  234. if flaskbb_config["TRACKER_LENGTH"] == 0:
  235. return False
  236. # check read_cutoff
  237. if topic.last_updated < read_cutoff:
  238. return False
  239. # topicsread is none if the user has marked the forum as read
  240. # or if he hasn't visited the topic yet
  241. if topicsread is None:
  242. # user has cleared the forum - check if there is a new post
  243. if forumsread and forumsread.cleared is not None:
  244. return forumsread.cleared < topic.last_updated
  245. # user hasn't read the topic yet, or there is a new post since the user
  246. # has marked the forum as read
  247. return True
  248. # check if there is a new post since the user's last topic visit
  249. return topicsread.last_read < topic.last_updated
  250. def mark_online(user_id, guest=False): # pragma: no cover
  251. """Marks a user as online
  252. :param user_id: The id from the user who should be marked as online
  253. :param guest: If set to True, it will add the user to the guest activity
  254. instead of the user activity.
  255. Ref: http://flask.pocoo.org/snippets/71/
  256. """
  257. user_id = to_bytes(user_id)
  258. now = int(time.time())
  259. expires = now + (flaskbb_config['ONLINE_LAST_MINUTES'] * 60) + 10
  260. if guest:
  261. all_users_key = 'online-guests/%d' % (now // 60)
  262. user_key = 'guest-activity/%s' % user_id
  263. else:
  264. all_users_key = 'online-users/%d' % (now // 60)
  265. user_key = 'user-activity/%s' % user_id
  266. p = redis_store.pipeline()
  267. p.sadd(all_users_key, user_id)
  268. p.set(user_key, now)
  269. p.expireat(all_users_key, expires)
  270. p.expireat(user_key, expires)
  271. p.execute()
  272. def get_online_users(guest=False): # pragma: no cover
  273. """Returns all online users within a specified time range
  274. :param guest: If True, it will return the online guests
  275. """
  276. current = int(time.time()) // 60
  277. minutes = range_method(flaskbb_config['ONLINE_LAST_MINUTES'])
  278. if guest:
  279. users = redis_store.sunion(['online-guests/%d' % (current - x)
  280. for x in minutes])
  281. else:
  282. users = redis_store.sunion(['online-users/%d' % (current - x)
  283. for x in minutes])
  284. return [to_unicode(u) for u in users]
  285. def crop_title(title, length=None, suffix="..."):
  286. """Crops the title to a specified length
  287. :param title: The title that should be cropped
  288. :param suffix: The suffix which should be appended at the
  289. end of the title.
  290. """
  291. length = flaskbb_config['TITLE_LENGTH'] if length is None else length
  292. if len(title) <= length:
  293. return title
  294. return title[:length].rsplit(' ', 1)[0] + suffix
  295. def is_online(user):
  296. """A simple check to see if the user was online within a specified
  297. time range
  298. :param user: The user who needs to be checked
  299. """
  300. return user.lastseen >= time_diff()
  301. def time_utcnow():
  302. """Returns a timezone aware utc timestamp."""
  303. return datetime.now(UTC)
  304. def time_diff():
  305. """Calculates the time difference between now and the ONLINE_LAST_MINUTES
  306. variable from the configuration.
  307. """
  308. now = time_utcnow()
  309. diff = now - timedelta(minutes=flaskbb_config['ONLINE_LAST_MINUTES'])
  310. return diff
  311. def _get_user_locale():
  312. locale = flaskbb_config.get("DEFAULT_LANGUAGE", "en")
  313. if current_user.is_authenticated and current_user.language is not None:
  314. locale = current_user.language
  315. return locale
  316. def _format_html_time_tag(datetime, what_to_display):
  317. if what_to_display == 'date-only':
  318. content = babel_format_date(datetime, locale=_get_user_locale())
  319. elif what_to_display == 'date-and-time':
  320. content = babel_format_datetime(
  321. datetime,
  322. tzinfo=UTC,
  323. locale=_get_user_locale()
  324. )
  325. # While this could be done with a format string, that would
  326. # hinder internationalization and honestly why bother.
  327. content += ' UTC'
  328. else:
  329. raise ValueError('what_to_display argument invalid')
  330. isoformat = datetime.isoformat()
  331. return Markup(
  332. '<time datetime="{}" data-what_to_display="{}">{}</time>'
  333. .format(isoformat, what_to_display, content)
  334. )
  335. def format_datetime(datetime):
  336. """Format the datetime for usage in templates.
  337. :param value: The datetime object that should be formatted
  338. :rtype: Markup
  339. """
  340. return _format_html_time_tag(datetime, 'date-and-time')
  341. def format_date(datetime, format=None):
  342. """Format the datetime for usage in templates, keeping only the date.
  343. :param value: The datetime object that should be formatted
  344. :rtype: Markup
  345. """
  346. if format:
  347. warnings.warn(
  348. 'This API has been deprecated due to i18n concerns. Please use '
  349. 'Jinja filters format_datetime and format_date without arguments '
  350. 'to format complete and date-only timestamps respectively.',
  351. DeprecationWarning
  352. )
  353. if '%H' in format:
  354. return format_datetime(datetime)
  355. return _format_html_time_tag(datetime, 'date-only')
  356. def format_timedelta(delta, **kwargs):
  357. """Wrapper around babel's format_timedelta to make it user language
  358. aware.
  359. """
  360. return babel_format_timedelta(delta, locale=_get_user_locale(), **kwargs)
  361. def time_since(time): # pragma: no cover
  362. """Returns a string representing time since e.g.
  363. 3 days ago, 5 hours ago.
  364. :param time: A datetime object
  365. """
  366. delta = time - time_utcnow()
  367. return format_timedelta(delta, add_direction=True)
  368. def format_quote(username, content):
  369. """Returns a formatted quote depending on the markup language.
  370. :param username: The username of a user.
  371. :param content: The content of the quote
  372. """
  373. profile_url = url_for('user.profile', username=username)
  374. content = "\n> ".join(content.strip().split('\n'))
  375. quote = u"**[{username}]({profile_url}) wrote:**\n> {content}\n".\
  376. format(username=username, profile_url=profile_url, content=content)
  377. return quote
  378. def get_image_info(url):
  379. """Returns the content-type, image size (kb), height and width of
  380. an image without fully downloading it.
  381. :param url: The URL of the image.
  382. """
  383. r = requests.get(url, stream=True)
  384. image_size = r.headers.get("content-length")
  385. image_size = float(image_size) / 1000 # in kilobyte
  386. image_max_size = 10000
  387. image_data = {
  388. "content_type": "",
  389. "size": image_size,
  390. "width": 0,
  391. "height": 0
  392. }
  393. # lets set a hard limit of 10MB
  394. if image_size > image_max_size:
  395. return image_data
  396. data = None
  397. parser = ImageFile.Parser()
  398. while True:
  399. data = r.raw.read(1024)
  400. if not data:
  401. break
  402. parser.feed(data)
  403. if parser.image:
  404. image_data["content_type"] = parser.image.format
  405. image_data["width"] = parser.image.size[0]
  406. image_data["height"] = parser.image.size[1]
  407. break
  408. return image_data
  409. def check_image(url):
  410. """A little wrapper for the :func:`get_image_info` function.
  411. If the image doesn't match the ``flaskbb_config`` settings it will
  412. return a tuple with a the first value is the custom error message and
  413. the second value ``False`` for not passing the check.
  414. If the check is successful, it will return ``None`` for the error message
  415. and ``True`` for the passed check.
  416. :param url: The image url to be checked.
  417. """
  418. img_info = get_image_info(url)
  419. error = None
  420. if img_info["size"] > flaskbb_config["AVATAR_SIZE"]:
  421. error = "Image is too big! {}kb are allowed.".format(
  422. flaskbb_config["AVATAR_SIZE"]
  423. )
  424. return error, False
  425. if not img_info["content_type"] in flaskbb_config["AVATAR_TYPES"]:
  426. error = "Image type {} is not allowed. Allowed types are: {}".format(
  427. img_info["content_type"],
  428. ", ".join(flaskbb_config["AVATAR_TYPES"])
  429. )
  430. return error, False
  431. if img_info["width"] > flaskbb_config["AVATAR_WIDTH"]:
  432. error = "Image is too wide! {}px width is allowed.".format(
  433. flaskbb_config["AVATAR_WIDTH"]
  434. )
  435. return error, False
  436. if img_info["height"] > flaskbb_config["AVATAR_HEIGHT"]:
  437. error = "Image is too high! {}px height is allowed.".format(
  438. flaskbb_config["AVATAR_HEIGHT"]
  439. )
  440. return error, False
  441. return error, True
  442. def get_alembic_locations(plugin_dirs):
  443. """Returns a tuple with (branchname, plugin_dir) combinations.
  444. The branchname is the name of plugin directory which should also be
  445. the unique identifier of the plugin.
  446. """
  447. branches_dirs = [
  448. tuple([os.path.basename(os.path.dirname(p)), p])
  449. for p in plugin_dirs
  450. ]
  451. return branches_dirs
  452. def get_available_themes():
  453. """Returns a list that contains all available themes. The items in the
  454. list are tuples where the first item of the tuple is the identifier and
  455. the second one the name of the theme.
  456. For example::
  457. [('aurora_mod', 'Aurora Mod')]
  458. """
  459. return [(theme.identifier, theme.name) for theme in get_themes_list()]
  460. def get_available_languages():
  461. """Returns a list that contains all available languages. The items in the
  462. list are tuples where the first item of the tuple is the locale
  463. identifier (i.e. de_AT) and the second one the display name of the locale.
  464. For example::
  465. [('de_AT', 'Deutsch (Österreich)')]
  466. """
  467. return [
  468. (get_locale_identifier((l.language, l.territory)), l.display_name)
  469. for l in babel.list_translations()
  470. ]
  471. def app_config_from_env(app, prefix="FLASKBB_"):
  472. """Retrieves the configuration variables from the environment.
  473. Set your environment variables like this::
  474. export FLASKBB_SECRET_KEY="your-secret-key"
  475. and based on the prefix, it will set the actual config variable
  476. on the ``app.config`` object.
  477. :param app: The application object.
  478. :param prefix: The prefix of the environment variables.
  479. """
  480. for key, value in iteritems(os.environ):
  481. if key.startswith(prefix):
  482. key = key[len(prefix):]
  483. try:
  484. value = ast.literal_eval(value)
  485. except (ValueError, SyntaxError):
  486. pass
  487. app.config[key] = value
  488. return app
  489. def get_flaskbb_config(app, config_file):
  490. """Returns ``None`` or the config which is either the path to a config file
  491. or an object. They can be used by ``app.config.from_pyfile`` or
  492. by ``app.config.from_object``.
  493. :param app: The app instance.
  494. :param config_file: A string which is either a module that can be
  495. imported, a path to a config file or an object.
  496. If the provided config_file can't be found, it will
  497. search for a 'flaskbb.cfg' file in the instance
  498. directory and in the project's root directory.
  499. """
  500. if config_file is not None:
  501. # config is an object
  502. if not isinstance(config_file, string_types):
  503. return config_file
  504. # config is a file
  505. if os.path.exists(os.path.join(app.instance_path, config_file)):
  506. return os.path.join(app.instance_path, config_file)
  507. if os.path.exists(os.path.abspath(config_file)):
  508. return os.path.join(os.path.abspath(config_file))
  509. # conifg is an importable string
  510. try:
  511. return import_string(config_file)
  512. except ImportStringError:
  513. return None
  514. else:
  515. # this would be so much nicer and cleaner if we wouldn't
  516. # support the root/project dir.
  517. # this walks back to flaskbb/ from flaskbb/flaskbb/cli/main.py
  518. project_dir = os.path.dirname(
  519. os.path.dirname(os.path.dirname(__file__))
  520. )
  521. project_config = os.path.join(project_dir, "flaskbb.cfg")
  522. # instance config doesn't exist
  523. instance_config = os.path.join(app.instance_path, "flaskbb.cfg")
  524. if os.path.exists(instance_config):
  525. return instance_config
  526. # config in root directory doesn't exist
  527. if os.path.exists(project_config):
  528. return project_config
  529. class ReverseProxyPathFix(object):
  530. """Wrap the application in this middleware and configure the
  531. front-end server to add these headers, to let you quietly bind
  532. this to a URL other than / and to an HTTP scheme that is
  533. different than what is used locally.
  534. http://flask.pocoo.org/snippets/35/
  535. In wsgi.py::
  536. from flaskbb.utils.helpers import ReverseProxyPathFix
  537. flaskbb = create_app(config="flaskbb.cfg")
  538. flaskbb.wsgi_app = ReverseProxyPathFix(flaskbb.wsgi_app)
  539. and in nginx::
  540. location /forums {
  541. proxy_pass http://127.0.0.1:8000;
  542. proxy_set_header Host $host;
  543. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  544. proxy_set_header X-Scheme $scheme;
  545. proxy_set_header X-Script-Name /forums; # this part here
  546. }
  547. :param app: the WSGI application
  548. :param force_https: Force HTTPS on all routes. Defaults to ``False``.
  549. """
  550. def __init__(self, app, force_https=False):
  551. self.app = app
  552. self.force_https = force_https
  553. def __call__(self, environ, start_response):
  554. script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
  555. if script_name:
  556. environ['SCRIPT_NAME'] = script_name
  557. path_info = environ.get('PATH_INFO', '')
  558. if path_info and path_info.startswith(script_name):
  559. environ['PATH_INFO'] = path_info[len(script_name):]
  560. server = environ.get('HTTP_X_FORWARDED_SERVER_CUSTOM',
  561. environ.get('HTTP_X_FORWARDED_SERVER', ''))
  562. if server:
  563. environ['HTTP_HOST'] = server
  564. scheme = environ.get('HTTP_X_SCHEME', '')
  565. if scheme:
  566. environ['wsgi.url_scheme'] = scheme
  567. if self.force_https:
  568. environ['wsgi.url_scheme'] = 'https'
  569. return self.app(environ, start_response)
  570. def real(obj):
  571. """Unwraps a werkzeug.local.LocalProxy object if given one,
  572. else returns the object.
  573. """
  574. if isinstance(obj, LocalProxy):
  575. return obj._get_current_object()
  576. return obj
  577. def parse_pkg_metadata(dist_name):
  578. try:
  579. raw_metadata = get_distribution(dist_name).get_metadata('METADATA')
  580. except FileNotFoundError:
  581. raw_metadata = get_distribution(dist_name).get_metadata('PKG-INFO')
  582. metadata = {}
  583. # lets use the Parser from email to parse our metadata :)
  584. for key, value in message_from_string(raw_metadata).items():
  585. metadata[key.replace('-', '_').lower()] = value
  586. return metadata
  587. def anonymous_required(f):
  588. @wraps(f)
  589. def wrapper(*a, **k):
  590. if current_user is not None and current_user.is_authenticated:
  591. return redirect_or_next(url_for('forum.index'))
  592. return f(*a, **k)
  593. return wrapper
  594. def enforce_recaptcha(limiter):
  595. current_limit = getattr(g, 'view_rate_limit', None)
  596. login_recaptcha = False
  597. if current_limit is not None:
  598. window_stats = limiter.limiter.get_window_stats(*current_limit)
  599. stats_diff = flaskbb_config["AUTH_REQUESTS"] - window_stats[1]
  600. login_recaptcha = stats_diff >= flaskbb_config["LOGIN_RECAPTCHA"]
  601. return login_recaptcha
  602. def registration_enabled(f):
  603. @wraps(f)
  604. def wrapper(*a, **k):
  605. if not flaskbb_config["REGISTRATION_ENABLED"]:
  606. flash(_("The registration has been disabled."), "info")
  607. return redirect_or_next(url_for("forum.index"))
  608. return f(*a, **k)
  609. return wrapper
  610. def requires_unactivated(f):
  611. @wraps(f)
  612. def wrapper(*a, **k):
  613. if current_user.is_active or not flaskbb_config["ACTIVATE_ACCOUNT"]:
  614. flash(_("This account is already activated."), "info")
  615. return redirect(url_for('forum.index'))
  616. return f(*a, **k)
  617. return wrapper
  618. def register_view(bp_or_app, routes, view_func, *args, **kwargs):
  619. for route in routes:
  620. bp_or_app.add_url_rule(route, view_func=view_func, *args, **kwargs)