589 lines
21 KiB
Python
589 lines
21 KiB
Python
from pyramid.config import Configurator
|
|
|
|
from sqlalchemy import engine_from_config
|
|
|
|
from .models import (
|
|
get_user_by_id,
|
|
get_or_create_user_by_email,
|
|
get_node_by_id,
|
|
get_or_create_node_by_uri,
|
|
get_nodes_who_share_root,
|
|
get_node_id_map,
|
|
get_graph_from_nodes,
|
|
get_or_create_namespace,
|
|
get_namespace_request_by_id,
|
|
get_conversation_graph_from_nodes,
|
|
flatten_graph,
|
|
)
|
|
|
|
# reject spammers.
|
|
from pyramid.httpexceptions import HTTPUnauthorized
|
|
|
|
# cookie only session, not encrypted but signed to prevent tampering!
|
|
from pyramid.session import SignedCookieSessionFactory
|
|
|
|
# email validation.
|
|
import re
|
|
|
|
# needed to load themes.
|
|
from pkg_resources import iter_entry_points
|
|
|
|
# needed to support expanding ENV vars from ini.
|
|
from os.path import expandvars
|
|
|
|
import logging
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
JINJA2_EXTENSION = ".j2"
|
|
|
|
|
|
def get_int_or_bool_or_none_or_str(value):
|
|
"""
|
|
Given a string value pulled from a configuration file,
|
|
this function attempts to return the value with the proper type.
|
|
"""
|
|
try:
|
|
return int(value)
|
|
except ValueError:
|
|
if value.lower() in {"yes", "y", "true", "y"}:
|
|
return True
|
|
elif value.lower() in {"no", "n", "false", "f"}:
|
|
return False
|
|
elif value.lower() == "none":
|
|
return None
|
|
return str(value)
|
|
|
|
|
|
def get_children_settings(settings, parent_key):
|
|
"""
|
|
Accept a settings dict and parent key, return dict of children
|
|
|
|
For example:
|
|
|
|
auth_tkt.hashalg = md5
|
|
|
|
Results to:
|
|
|
|
{'auth_tkt.hashalg': 'md5'}
|
|
|
|
This function returns the following:
|
|
|
|
>>> get_children_settings({'auth_tkt.hashalg': 'md5'}, 'auth_tkt')
|
|
{'hashalg': 'md5'}
|
|
|
|
"""
|
|
# needed to support expanding ENV vars from ini.
|
|
from os.path import expandvars
|
|
|
|
# the +1 is the . between parent and child settings.
|
|
parent_len = len(parent_key) + 1
|
|
children = {}
|
|
for key, value in settings.items():
|
|
if parent_key in key:
|
|
# expandvars replaces template with ENV vars.
|
|
children[key[parent_len:]] = get_int_or_bool_or_none_or_str(
|
|
expandvars(value)
|
|
)
|
|
return children
|
|
|
|
|
|
def load_entry_points(group_name):
|
|
"""Return a dictionary of entry_points related to given group_name"""
|
|
entry_points = {}
|
|
for entry_point in iter_entry_points(group=group_name, name=None):
|
|
entry_points[entry_point.name] = entry_point.load()
|
|
return entry_points
|
|
|
|
|
|
def load_jinja2_themes(config):
|
|
"""Automatically load any entry_point registered Remarkbox theme."""
|
|
themes = load_entry_points("remarkbox.themes")
|
|
theme_defaults = {}
|
|
|
|
for theme_name, theme_module in themes.items():
|
|
theme_module_name = theme_module.__name__
|
|
# teach Jinja2 about the template dir in the theme package.
|
|
config.add_jinja2_search_path(
|
|
"{}:templates/".format(theme_module_name), name=JINJA2_EXTENSION
|
|
)
|
|
# teach Pyramid about the static assets in the theme package.
|
|
config.add_static_view(
|
|
"static/theme/{}".format(theme_name),
|
|
"{}:static/theme/{}".format(theme_module_name, theme_name),
|
|
cache_max_age=3600,
|
|
)
|
|
# Collect theme's default mode if defined
|
|
if hasattr(theme_module, 'default_theme_mode'):
|
|
theme_defaults[theme_name] = theme_module.default_theme_mode
|
|
|
|
# Store theme defaults in config registry for later access
|
|
config.registry.settings['theme_defaults'] = theme_defaults
|
|
return config
|
|
|
|
|
|
def is_ipv4(string):
|
|
try:
|
|
parts = list(map(int, string.split(".")))
|
|
except:
|
|
return False
|
|
if len(parts) != 4:
|
|
return False
|
|
for part in parts:
|
|
if part < 0 or part > 256:
|
|
return False
|
|
return True
|
|
|
|
|
|
def maybe_root_domain(string):
|
|
"""
|
|
Maybe get valid "root" domain from given string, for example -
|
|
|
|
valid:
|
|
|
|
* hi.www.example.com -> example.com
|
|
* www.example.com -> example.com
|
|
* example.com -> example.com
|
|
|
|
invalid:
|
|
|
|
* comments.cryptocoin.com.au -> com.au
|
|
"""
|
|
return ".".join(string.split(".")[-2:])
|
|
|
|
|
|
def main(global_config, **settings):
|
|
"""This function returns a Pyramid WSGI application."""
|
|
|
|
app_settings = get_children_settings(settings, "app")
|
|
session_settings = get_children_settings(settings, "session")
|
|
|
|
def wild_signed_cookie_session_factory(request):
|
|
"""
|
|
Hack SignedCookieSessionFactory to have `wild_domain`.
|
|
Pyramid devs do not want to support this.
|
|
|
|
In addition the domain is determined per request!
|
|
|
|
This means a single deployment of this app can support managing a
|
|
`wild_domain` cookie for any domain (likely using CNAMES).
|
|
"""
|
|
root_domain = maybe_root_domain(request.domain)
|
|
if is_ipv4(request.domain) or root_domain.startswith("com"):
|
|
session_settings["domain"] = request.domain
|
|
else:
|
|
session_settings["domain"] = root_domain
|
|
|
|
factory = SignedCookieSessionFactory(**session_settings)
|
|
return factory(request)
|
|
|
|
# setup session factory to use unencrypted but signed cookies.
|
|
# session_factory = SignedCookieSessionFactory(**session_settings)
|
|
session_factory = wild_signed_cookie_session_factory
|
|
|
|
# build app config object from ini.
|
|
config = Configurator(settings=settings, session_factory=session_factory)
|
|
|
|
# setup and require automatic CSRF checking.
|
|
config.set_default_csrf_options(require_csrf=True)
|
|
|
|
# Create database engine from connection details in ini.
|
|
# make request.dbsession available for use in Pyramid.
|
|
config.include(".models")
|
|
|
|
# setup jinja2 template support.
|
|
config.include("pyramid_jinja2")
|
|
config.add_jinja2_search_path("remarkbox:templates/", name=JINJA2_EXTENSION)
|
|
config.add_jinja2_renderer(JINJA2_EXTENSION)
|
|
config = load_jinja2_themes(config)
|
|
|
|
# compile the email validator regex outside of the functions.
|
|
_email_regex = re.compile("^[^@]+@[^@]+\.[^.@]+$")
|
|
|
|
def add_debug_mode(request):
|
|
"""Return True if debug toolbar is enabled."""
|
|
return "pyramid_debugtoolbar" in request.registry.settings.get(
|
|
"pyramid.includes", ""
|
|
)
|
|
|
|
'''
|
|
def add_redis(request):
|
|
"""Return Redis Connection"""
|
|
redis_host = request.app.get("redis.host", "localhost")
|
|
redis_port = request.app.get("redis.port", 6379)
|
|
redis_db = request.app.get("redis.db", 0)
|
|
return redis.Redis(host=redis_host, port=redis_port, db=redis_db)
|
|
'''
|
|
|
|
def add_email(request):
|
|
"""Return Email or None. Email must pass regex."""
|
|
email = request.params.get("email", "")
|
|
if _email_regex.match(email) is not None:
|
|
return email
|
|
|
|
def add_user(request):
|
|
"""Return User object or None. User.authenticated may be True or False."""
|
|
user = None
|
|
authenticated_user_id = request.session.get("authenticated_user_id", None)
|
|
|
|
if authenticated_user_id:
|
|
# attach the user object from DB to the request.
|
|
user = get_user_by_id(request.dbsession, authenticated_user_id)
|
|
if user is not None:
|
|
user.authenticated = True
|
|
|
|
elif request.email:
|
|
user = get_or_create_user_by_email(request.dbsession, request.email)
|
|
|
|
return user
|
|
|
|
def add_csrf_token(request):
|
|
if request.user and request.user.authenticated:
|
|
return request.session.get_csrf_token()
|
|
|
|
def add_node(request):
|
|
"""Return Node object or None from matchdict or params."""
|
|
thread_uri = request.params.get("thread_uri")
|
|
thread_title = request.params.get("thread_title")
|
|
node_id = request.matchdict.get("node_id")
|
|
nojs = request.params.get("nojs", False)
|
|
|
|
node = None
|
|
|
|
# TODO: thread_uri must take priority over node_id because our forms
|
|
# are dumb. If we fix our forms to only pass node_id/node_path when not
|
|
# None, the priority / order will not matter.
|
|
if thread_uri:
|
|
# Remove "/reply" from the end of the URL if present
|
|
url = request.url
|
|
if url.endswith("/reply"):
|
|
url = url[:-6] # Remove the last 6 characters ("/reply")
|
|
request.session["back_to_thread"] = url
|
|
|
|
node = get_or_create_node_by_uri(
|
|
request.dbsession, thread_uri, thread_title
|
|
)
|
|
elif node_id:
|
|
node = get_node_by_id(request.dbsession, node_id)
|
|
elif nojs and request.referer:
|
|
# if javascript is disabled, use the referer.
|
|
node = get_or_create_node_by_uri(request.dbsession, request.referer)
|
|
|
|
return node
|
|
|
|
def add_root_node(request):
|
|
return request.node.root
|
|
|
|
def add_nodes(request):
|
|
if request.node:
|
|
return get_nodes_who_share_root(
|
|
request.dbsession,
|
|
request.node.root,
|
|
request.node_order,
|
|
)
|
|
|
|
def add_node_id_map(request):
|
|
return get_node_id_map(request.nodes)
|
|
|
|
def add_node_graph(request):
|
|
return get_graph_from_nodes(request.nodes)
|
|
|
|
def add_node_flat_graph(request):
|
|
return flatten_graph(request.node.root.id, request.node_graph)
|
|
|
|
def add_conversation_graph(request):
|
|
return get_conversation_graph_from_nodes(
|
|
request.nodes,
|
|
graph=request.node_graph,
|
|
node_id_map=request.node_id_map,
|
|
order=request.node_order,
|
|
)
|
|
|
|
def add_namespace(request):
|
|
namespace_name = request.matchdict.get(
|
|
"namespace", request.params.get("namespace", None)
|
|
)
|
|
if namespace_name:
|
|
return get_or_create_namespace(request.dbsession, namespace_name)
|
|
elif request.node:
|
|
return request.node.root.namespace
|
|
return get_or_create_namespace(request.dbsession, request.domain)
|
|
|
|
def add_mode(request):
|
|
"""return mode of 'embed' or 'basic'"""
|
|
return "embed" if "embed" in request.matched_route.name else "basic"
|
|
|
|
def add_link_prefix(request):
|
|
if request.mode == "embed":
|
|
return "/embed/ns/{}".format(request.namespace.name)
|
|
return ""
|
|
|
|
def add_spam(request):
|
|
"""Test if request looks spammy HTTP Error or False."""
|
|
if request.params.get("email2", "") != "":
|
|
# spam filter, hidden email2 field tricks bots.
|
|
return HTTPUnauthorized("you smell like a spammer")
|
|
return False
|
|
|
|
def add_owner_key(request):
|
|
return request.params.get("rb_owner_key", None)
|
|
|
|
def add_namespace_request(request):
|
|
"""Add namespace_request if pending, else None"""
|
|
# we reduce database lookups by only querying when owner_key is
|
|
# present and request.namespace has a pending owner_request.
|
|
if request.owner_key and request.namespace.owner_request_pending:
|
|
namespace_request = get_namespace_request_by_id(
|
|
request.dbsession, request.owner_key
|
|
)
|
|
# for dubugging purposes only.
|
|
# log.info("owner_key={}, pending={}".format(request.owner_key,request.namespace.owner_request_pending))
|
|
if namespace_request and namespace_request.namespace == request.namespace:
|
|
return namespace_request
|
|
|
|
def add_app(request):
|
|
"""Attach app settings dictionary."""
|
|
return app_settings
|
|
|
|
def add_app_url(request):
|
|
"""
|
|
Use the app_url from config if request.domain ends with the
|
|
configured root_domain. Otherwise just use the request's host_url
|
|
"""
|
|
config_app_url = request.app.get("app_url", request.host_url)
|
|
root_domain = request.app.get("root_domain")
|
|
if root_domain and request.domain.endswith(root_domain):
|
|
return config_app_url
|
|
elif request.domain == "localhost":
|
|
return config_app_url
|
|
return request.host_url
|
|
|
|
def add_app_domain(request):
|
|
return request.app_url.split("://")[-1].split(":")[0]
|
|
|
|
def add_marketing_url(request):
|
|
return request.app.get("marketing_url", request.app_url)
|
|
|
|
def add_marketing_domain(request):
|
|
return request.marketing_url.split("://")[-1].split(":")[0]
|
|
|
|
def add_faq_home(request):
|
|
"""Return True if this request's domain equals namespace name."""
|
|
return request.domain == request.namespace.name
|
|
|
|
def add_saas_home(request):
|
|
"""Only one domain should have this method return True per deployment."""
|
|
root_domain = request.app.get("root_domain")
|
|
return (
|
|
root_domain
|
|
and request.domain.endswith(root_domain)
|
|
and request.app_domain == request.namespace.name
|
|
)
|
|
|
|
def add_stripe(request):
|
|
"""Attach a stripe object with creds to request."""
|
|
import stripe
|
|
|
|
stripe.api_key = request.app.get("stripe.secret")
|
|
return stripe
|
|
|
|
def add_stripe_customer(request):
|
|
if request.user:
|
|
if not request.user.stripe_id:
|
|
# create a new stripe customer.
|
|
customer = request.stripe.Customer.create(email=request.user.email)
|
|
request.user.stripe_id = customer.id
|
|
request.dbsession.add(request.user)
|
|
request.dbsession.flush()
|
|
return request.stripe.Customer.retrieve(request.user.stripe_id)
|
|
return None
|
|
|
|
def add_stripe_saved_cards(request):
|
|
if request.user and request.user.stripe_id:
|
|
return request.stripe_customer.sources
|
|
return []
|
|
|
|
def add_stripe_active_card(request):
|
|
if request.user and request.user.stripe_id:
|
|
if request.stripe_customer.default_source:
|
|
return request.stripe_customer.sources.retrieve(
|
|
request.stripe_customer.default_source
|
|
)
|
|
return None
|
|
|
|
def add_avatar_size(request):
|
|
"""Attach avatar size or default."""
|
|
if request.namespace:
|
|
if request.namespace.avatar_size:
|
|
return request.namespace.avatar_size
|
|
return request.app.get("avatar.size", 30)
|
|
|
|
def add_stand_alone_mode(request):
|
|
if request.app.get("stand_alone_mode", "disabled") == "enabled":
|
|
return True
|
|
return False
|
|
|
|
def add_theme(request):
|
|
"""Attach theme name or None."""
|
|
if request.mode == "embed":
|
|
if request.namespace and request.namespace.theme_embed:
|
|
return request.namespace.theme_embed
|
|
return request.app.get("theme_embed", None)
|
|
|
|
if request.namespace and request.namespace.theme:
|
|
return request.namespace.theme
|
|
return request.app.get("theme", None)
|
|
|
|
def add_stylesheet(request):
|
|
"""Attach stylesheet or empty string."""
|
|
if request.mode == "embed":
|
|
return request.stylesheet_embed
|
|
return request.stylesheet_basic
|
|
|
|
def add_stylesheet_basic(request):
|
|
"""Attach stylesheet or empty string."""
|
|
if request.namespace and request.namespace.stylesheet:
|
|
return request.namespace.stylesheet
|
|
return ""
|
|
|
|
def add_stylesheet_embed(request):
|
|
"""Attach stylesheet_embed or empty string."""
|
|
if request.namespace and request.namespace.stylesheet_embed:
|
|
return request.namespace.stylesheet_embed
|
|
return ""
|
|
|
|
def add_stylesheet_uri(request):
|
|
"""Attach stylesheet uri or empty string."""
|
|
if request.mode == "embed":
|
|
return request.stylesheet_embed_uri
|
|
return request.stylesheet_basic_uri
|
|
|
|
def add_stylesheet_basic_uri(request):
|
|
"""Attach stylesheet uri or empty string."""
|
|
if request.namespace and request.namespace.stylesheet_uri:
|
|
return request.namespace.stylesheet_uri
|
|
return ""
|
|
|
|
def add_stylesheet_embed_uri(request):
|
|
"""Attach stylesheet_embed uri or empty string."""
|
|
if request.namespace and request.namespace.stylesheet_embed_uri:
|
|
return request.namespace.stylesheet_embed_uri
|
|
return ""
|
|
|
|
def add_base_template(request):
|
|
"""Attach base_template filename."""
|
|
if request.theme:
|
|
return "{}-base.j2".format(request.theme)
|
|
return "base.j2"
|
|
|
|
def add_base_funnel_template(request):
|
|
"""Attach base_funnel template filename."""
|
|
if request.theme:
|
|
return "{}-base-funnel.j2".format(request.theme)
|
|
return "base.j2"
|
|
|
|
def add_page_number(request):
|
|
"""Attach page_number starting at 0"""
|
|
page_number = int(request.params.get("page", 1))
|
|
return page_number if page_number >= 1 else 1
|
|
|
|
def add_page_size(request):
|
|
# if we want to support custom page sizes, set it here.
|
|
return 100
|
|
|
|
def add_page_offset(request):
|
|
return (request.page_number - 1) * request.page_size
|
|
|
|
def add_node_order(request):
|
|
return request.params.get("order", request.namespace.node_order)
|
|
|
|
def add_mathjax(request):
|
|
return "true" if request.namespace.mathjax else "false"
|
|
|
|
def add_theme_mode(request):
|
|
"""
|
|
Return theme mode 'light' or 'dark'.
|
|
Priority: user preference > query params > theme default > 'light'.
|
|
"""
|
|
# If user is authenticated and has a preference
|
|
if request.user and request.user.authenticated and request.user.theme_mode != 'auto':
|
|
return request.user.theme_mode
|
|
|
|
# Check if there's a mode parameter (for embeds or overrides)
|
|
param_mode = request.params.get("mode")
|
|
if param_mode in ("light", "dark"):
|
|
return param_mode
|
|
|
|
# Use theme's default mode if available
|
|
if request.theme:
|
|
theme_defaults = request.registry.settings.get('theme_defaults', {})
|
|
theme_default = theme_defaults.get(request.theme)
|
|
if theme_default in ("light", "dark"):
|
|
return theme_default
|
|
|
|
# Final fallback to light
|
|
return "light"
|
|
|
|
# register functions to app config as request methods.
|
|
# each request instance will run these functions and attach results.
|
|
# cache result with `reify=True` to prevent multiple db lookups.
|
|
# config.add_request_method(add_redis, "redis", reify=True)
|
|
config.add_request_method(add_debug_mode, "debug_mode", reify=True)
|
|
config.add_request_method(add_user, "user", reify=True)
|
|
config.add_request_method(add_csrf_token, "csrf_token", reify=True)
|
|
config.add_request_method(add_email, "email", reify=True)
|
|
config.add_request_method(add_node, "node", reify=True)
|
|
config.add_request_method(add_root_node, "root_node", reify=True)
|
|
config.add_request_method(add_nodes, "nodes", reify=True)
|
|
config.add_request_method(add_node_id_map, "node_id_map", reify=True)
|
|
config.add_request_method(add_node_graph, "node_graph", reify=True)
|
|
config.add_request_method(add_node_flat_graph, "node_flat_graph", reify=True)
|
|
config.add_request_method(add_conversation_graph, "conversation_graph", reify=True)
|
|
config.add_request_method(add_namespace, "namespace", reify=True)
|
|
config.add_request_method(add_mode, "mode", reify=True)
|
|
config.add_request_method(add_link_prefix, "link_prefix", reify=True)
|
|
config.add_request_method(add_spam, "spam", reify=True)
|
|
config.add_request_method(add_owner_key, "owner_key", reify=True)
|
|
config.add_request_method(add_namespace_request, "namespace_request", reify=True)
|
|
config.add_request_method(add_app, "app", reify=True)
|
|
config.add_request_method(add_app_url, "app_url", reify=True)
|
|
config.add_request_method(add_app_domain, "app_domain", reify=True)
|
|
config.add_request_method(add_marketing_url, "marketing_url", reify=True)
|
|
config.add_request_method(add_marketing_domain, "marketing_domain", reify=True)
|
|
config.add_request_method(add_faq_home, "faq_home", reify=True)
|
|
config.add_request_method(add_saas_home, "saas_home", reify=True)
|
|
config.add_request_method(add_stripe, "stripe", reify=True)
|
|
config.add_request_method(add_stripe_customer, "stripe_customer", reify=True)
|
|
config.add_request_method(add_stripe_saved_cards, "stripe_saved_cards", reify=True)
|
|
config.add_request_method(add_stripe_active_card, "stripe_active_card", reify=True)
|
|
config.add_request_method(add_stand_alone_mode, "stand_alone_mode", reify=True)
|
|
config.add_request_method(add_avatar_size, "avatar_size", reify=True)
|
|
config.add_request_method(add_theme, "theme", reify=True)
|
|
config.add_request_method(add_stylesheet_basic, "stylesheet_basic", reify=True)
|
|
config.add_request_method(add_stylesheet_embed, "stylesheet_embed", reify=True)
|
|
config.add_request_method(add_stylesheet, "stylesheet", reify=True)
|
|
config.add_request_method(
|
|
add_stylesheet_basic_uri, "stylesheet_basic_uri", reify=True
|
|
)
|
|
config.add_request_method(
|
|
add_stylesheet_embed_uri, "stylesheet_embed_uri", reify=True
|
|
)
|
|
config.add_request_method(add_stylesheet_uri, "stylesheet_uri", reify=True)
|
|
config.add_request_method(add_base_template, "base_template", reify=True)
|
|
config.add_request_method(
|
|
add_base_funnel_template, "base_funnel_template", reify=True
|
|
)
|
|
config.add_request_method(add_page_number, "page_number", reify=True)
|
|
config.add_request_method(add_page_size, "page_size", reify=True)
|
|
config.add_request_method(add_page_offset, "page_offset", reify=True)
|
|
config.add_request_method(add_node_order, "node_order", reify=True)
|
|
config.add_request_method(add_mathjax, "mathjax", reify=True)
|
|
config.add_request_method(add_theme_mode, "theme_mode", reify=True)
|
|
|
|
# all of the web application routes.
|
|
config.include(".routes")
|
|
|
|
# Scan for views.
|
|
config.scan()
|
|
|
|
return config.make_wsgi_app()
|