123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 |
- # -*- coding: utf-8 -*-
- """
- flaskbb.utils.helpers
- ~~~~~~~~~~~~~~~~~~~~
- A few helpers that are used by flaskbb
- :copyright: (c) 2014 by the FlaskBB Team.
- :license: BSD, see LICENSE for more details.
- """
- import re
- import time
- import itertools
- import operator
- import struct
- from io import BytesIO
- from datetime import datetime, timedelta
- import requests
- from flask import session, url_for
- from babel.dates import format_timedelta
- from flask_themes2 import render_theme_template
- from flask_login import current_user
- import unidecode
- from flaskbb._compat import range_method, text_type
- from flaskbb.extensions import redis_store
- from flaskbb.utils.settings import flaskbb_config
- from flaskbb.utils.markup import markdown
- _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
- def slugify(text, delim=u'-'):
- """Generates an slightly worse ASCII-only slug.
- Taken from the Flask Snippets page.
- :param text: The text which should be slugified
- :param delim: Default "-". The delimeter for whitespace
- """
- text = unidecode.unidecode(text)
- result = []
- for word in _punct_re.split(text.lower()):
- if word:
- result.append(word)
- return text_type(delim.join(result))
- def render_template(template, **context): # pragma: no cover
- """A helper function that uses the `render_theme_template` function
- without needing to edit all the views
- """
- if current_user.is_authenticated() and current_user.theme:
- theme = current_user.theme
- else:
- theme = session.get('theme', flaskbb_config['DEFAULT_THEME'])
- return render_theme_template(theme, template, **context)
- def get_categories_and_forums(query_result, user):
- """Returns a list with categories. Every category has a list for all
- their associated forums.
- The structure looks like this::
- [(<Category 1>,
- [(<Forum 1>, None),
- (<Forum 2>, <flaskbb.forum.models.ForumsRead at 0x38fdb50>)]),
- (<Category 2>,
- [(<Forum 3>, None),
- (<Forum 4>, None)])]
- and to unpack the values you can do this::
- In [110]: for category, forums in x:
- .....: print category
- .....: for forum, forumsread in forums:
- .....: print "\t", forum, forumsread
- This will print something like this:
- <Category 1>
- <Forum 1> None
- <Forum 2> <flaskbb.forum.models.ForumsRead object at 0x38fdb50>
- <Category 2>
- <Forum 3> None
- <Forum 4> None
- :param query_result: A tuple (KeyedTuple) with all categories and forums
- :param user: The user object is needed because a signed out user does not
- have the ForumsRead relation joined.
- """
- it = itertools.groupby(query_result, operator.itemgetter(0))
- forums = []
- if user.is_authenticated():
- for key, value in it:
- forums.append((key, [(item[1], item[2]) for item in value]))
- else:
- for key, value in it:
- forums.append((key, [(item[1], None) for item in value]))
- return forums
- def get_forums(query_result, user):
- """Returns a tuple which contains the category and the forums as list.
- This is the counterpart for get_categories_and_forums and especially
- usefull when you just need the forums for one category.
- For example::
- (<Category 2>,
- [(<Forum 3>, None),
- (<Forum 4>, None)])
- :param query_result: A tuple (KeyedTuple) with all categories and forums
- :param user: The user object is needed because a signed out user does not
- have the ForumsRead relation joined.
- """
- it = itertools.groupby(query_result, operator.itemgetter(0))
- if user.is_authenticated():
- for key, value in it:
- forums = key, [(item[1], item[2]) for item in value]
- else:
- for key, value in it:
- forums = key, [(item[1], None) for item in value]
- return forums
- def forum_is_unread(forum, forumsread, user):
- """Checks if a forum is unread
- :param forum: The forum that should be checked if it is unread
- :param forumsread: The forumsread object for the forum
- :param user: The user who should be checked if he has read the forum
- """
- # If the user is not signed in, every forum is marked as read
- if not user.is_authenticated():
- return False
- read_cutoff = datetime.utcnow() - timedelta(
- days=flaskbb_config["TRACKER_LENGTH"])
- # disable tracker if TRACKER_LENGTH is set to 0
- if flaskbb_config["TRACKER_LENGTH"] == 0:
- return False
- # If there are no topics in the forum, mark it as read
- if forum and forum.topic_count == 0:
- return False
- # If the user hasn't visited a topic in the forum - therefore,
- # forumsread is None and we need to check if it is still unread
- if forum and not forumsread:
- return forum.last_post_created > read_cutoff
- try:
- # check if the forum has been cleared and if there is a new post
- # since it have been cleared
- if forum.last_post_created > forumsread.cleared:
- if forum.last_post_created < forumsread.last_read:
- return False
- except TypeError:
- pass
- # else just check if the user has read the last post
- return forum.last_post_created > forumsread.last_read
- def topic_is_unread(topic, topicsread, user, forumsread=None):
- """Checks if a topic is unread.
- :param topic: The topic that should be checked if it is unread
- :param topicsread: The topicsread object for the topic
- :param user: The user who should be checked if he has read the last post
- in the topic
- :param forumsread: The forumsread object in which the topic is. If you
- also want to check if the user has marked the forum as
- read, than you will also need to pass an forumsread
- object.
- """
- if not user.is_authenticated():
- return False
- read_cutoff = datetime.utcnow() - timedelta(
- days=flaskbb_config["TRACKER_LENGTH"])
- # disable tracker if read_cutoff is set to 0
- if flaskbb_config["TRACKER_LENGTH"] == 0:
- return False
- # check read_cutoff
- if topic.last_post.date_created < read_cutoff:
- return False
- # topicsread is none if the user has marked the forum as read
- # or if he hasn't visited yet
- if topicsread is None:
- # user has cleared the forum sometime ago - check if there is a new post
- if forumsread and forumsread.cleared is not None:
- return forumsread.cleared < topic.last_post.date_created
- # user hasn't read the topic yet, or there is a new post since the user
- # has marked the forum as read
- return True
- # check if there is a new post since the user's last topic visit
- return topicsread.last_read < topic.last_post.date_created
- def mark_online(user_id, guest=False): # pragma: no cover
- """Marks a user as online
- :param user_id: The id from the user who should be marked as online
- :param guest: If set to True, it will add the user to the guest activity
- instead of the user activity.
- Ref: http://flask.pocoo.org/snippets/71/
- """
- now = int(time.time())
- expires = now + (flaskbb_config['ONLINE_LAST_MINUTES'] * 60) + 10
- if guest:
- all_users_key = 'online-guests/%d' % (now // 60)
- user_key = 'guest-activity/%s' % user_id
- else:
- all_users_key = 'online-users/%d' % (now // 60)
- user_key = 'user-activity/%s' % user_id
- p = redis_store.pipeline()
- p.sadd(all_users_key, user_id)
- p.set(user_key, now)
- p.expireat(all_users_key, expires)
- p.expireat(user_key, expires)
- p.execute()
- def get_online_users(guest=False): # pragma: no cover
- """Returns all online users within a specified time range
- :param guest: If True, it will return the online guests
- """
- current = int(time.time()) // 60
- minutes = range_method(flaskbb_config['ONLINE_LAST_MINUTES'])
- if guest:
- return redis_store.sunion(['online-guests/%d' % (current - x)
- for x in minutes])
- return redis_store.sunion(['online-users/%d' % (current - x)
- for x in minutes])
- def crop_title(title, suffix="..."):
- """Crops the title to a specified length
- :param title: The title that should be cropped
- :param suffix: The suffix which should be appended at the
- end of the title.
- """
- length = flaskbb_config['TITLE_LENGTH']
- if len(title) <= length:
- return title
- return title[:length].rsplit(' ', 1)[0] + suffix
- def render_markup(text):
- """Renders the given text as markdown
- :param text: The text that should be rendered as markdown
- """
- return markdown.render(text)
- def is_online(user):
- """A simple check to see if the user was online within a specified
- time range
- :param user: The user who needs to be checked
- """
- return user.lastseen >= time_diff()
- def time_diff():
- """Calculates the time difference between now and the ONLINE_LAST_MINUTES
- variable from the configuration.
- """
- now = datetime.utcnow()
- diff = now - timedelta(minutes=flaskbb_config['ONLINE_LAST_MINUTES'])
- return diff
- def format_date(value, format='%Y-%m-%d'):
- """Returns a formatted time string
- :param value: The datetime object that should be formatted
- :param format: How the result should look like. A full list of available
- directives is here: http://goo.gl/gNxMHE
- """
- return value.strftime(format)
- def time_since(time): # pragma: no cover
- """Returns a string representing time since e.g.
- 3 days ago, 5 hours ago.
- :param time: A datetime object
- """
- delta = time - datetime.utcnow()
- locale = "en"
- if current_user.is_authenticated() and current_user.language is not None:
- locale = current_user.language
- return format_timedelta(delta, add_direction=True, locale=locale)
- def format_quote(username, content):
- """Returns a formatted quote depending on the markup language.
- :param username: The username of a user.
- :param content: The content of the quote
- """
- profile_url = url_for('user.profile', username=username)
- content = "\n> ".join(content.strip().split('\n'))
- quote = "**[{username}]({profile_url}) wrote:**\n> {content}\n".\
- format(username=username, profile_url=profile_url, content=content)
- return quote
- def get_image_info(url):
- """Returns the content-type, image size (kb), height and width of a image
- without fully downloading it. It will just download the first 1024 bytes.
- LICENSE: New BSD License (taken from the start page of the repository)
- https://code.google.com/p/bfg-pages/source/browse/trunk/pages/getimageinfo.py
- """
- r = requests.get(url, stream=True)
- image_size = r.headers.get("content-length")
- image_size = float(image_size) / 1000 # in kilobyte
- data = r.raw.read(1024)
- size = len(data)
- height = -1
- width = -1
- content_type = ''
- if size:
- size = int(size)
- # handle GIFs
- if (size >= 10) and data[:6] in (b'GIF87a', b'GIF89a'):
- # Check to see if content_type is correct
- content_type = 'image/gif'
- w, h = struct.unpack(b'<HH', data[6:10])
- width = int(w)
- height = int(h)
- # See PNG 2. Edition spec (http://www.w3.org/TR/PNG/)
- # Bytes 0-7 are below, 4-byte chunk length, then 'IHDR'
- # and finally the 4-byte width, height
- elif ((size >= 24) and data.startswith(b'\211PNG\r\n\032\n') and
- (data[12:16] == b'IHDR')):
- content_type = 'image/png'
- w, h = struct.unpack(b">LL", data[16:24])
- width = int(w)
- height = int(h)
- # Maybe this is for an older PNG version.
- elif (size >= 16) and data.startswith(b'\211PNG\r\n\032\n'):
- # Check to see if we have the right content type
- content_type = 'image/png'
- w, h = struct.unpack(b">LL", data[8:16])
- width = int(w)
- height = int(h)
- # handle JPEGs
- elif (size >= 2) and data.startswith(b'\377\330'):
- content_type = 'image/jpeg'
- jpeg = BytesIO(data)
- jpeg.read(2)
- b = jpeg.read(1)
- try:
- while (b and ord(b) != 0xDA):
- while (ord(b) != 0xFF):
- b = jpeg.read(1)
- while (ord(b) == 0xFF):
- b = jpeg.read(1)
- if (ord(b) >= 0xC0 and ord(b) <= 0xC3):
- jpeg.read(3)
- h, w = struct.unpack(b">HH", jpeg.read(4))
- break
- else:
- jpeg.read(int(struct.unpack(b">H", jpeg.read(2))[0])-2)
- b = jpeg.read(1)
- width = int(w)
- height = int(h)
- except struct.error:
- pass
- except ValueError:
- pass
- return {"content-type": content_type, "size": image_size,
- "width": width, "height": height}
- def check_image(url):
- """A little wrapper for the :func:`get_image_info` function.
- If the image doesn't match the ``flaskbb_config`` settings it will
- return a tuple with a the first value is the custom error message and
- the second value ``False`` for not passing the check.
- If the check is successful, it will return ``None`` for the error message
- and ``True`` for the passed check.
- :param url: The image url to be checked.
- """
- img_info = get_image_info(url)
- error = None
- if not img_info["content-type"] in flaskbb_config["AVATAR_TYPES"]:
- error = "Image type is not allowed. Allowed types are: {}".format(
- ", ".join(flaskbb_config["AVATAR_TYPES"])
- )
- return error, False
- if img_info["width"] > flaskbb_config["AVATAR_WIDTH"]:
- error = "Image is too wide! {}px width is allowed.".format(
- flaskbb_config["AVATAR_WIDTH"]
- )
- return error, False
- if img_info["height"] > flaskbb_config["AVATAR_HEIGHT"]:
- error = "Image is too high! {}px height is allowed.".format(
- flaskbb_config["AVATAR_HEIGHT"]
- )
- return error, False
- if img_info["size"] > flaskbb_config["AVATAR_SIZE"]:
- error = "Image is too big! {}kb are allowed.".format(
- flaskbb_config["AVATAR_SIZE"]
- )
- return error, False
- return error, True
|