tailbone/tailbone/subscribers.py
Lance Edgar 2f8411ba2f Add has_perm() etc. to request during the NewRequest event
still get the occasional server error when handling what should be a
simple 404 request e.g. for /wp-login.php

error indicates there is no `request.has_perm()` at the time, so
hoping this moves it earlier in the life cycle so it *will* exist..
2023-03-25 01:03:49 -05:00

271 lines
10 KiB
Python

# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Event Subscribers
"""
import six
import json
import datetime
import rattail
import colander
import deform
from pyramid import threadlocal
from webhelpers2.html import tags
import tailbone
from tailbone import helpers
from tailbone.db import Session
from tailbone.config import csrf_header_name, should_expose_websockets
from tailbone.menus import make_simple_menus
from tailbone.util import get_global_search_options
def new_request(event):
"""
Identify the current user, and cache their current permissions. Also adds
the ``rattail_config`` attribute to the request.
A global Rattail ``config`` should already be present within the Pyramid
application registry's settings, which would normally be accessed via::
request.registry.settings['rattail_config']
This function merely "promotes" that config object so that it is more
directly accessible, a la::
request.rattail_config
.. note::
This of course assumes that a Rattail ``config`` object *has* in fact
already been placed in the application registry settings. If this is
not the case, this function will do nothing.
Also, attach some goodies to the request object:
* The currently logged-in user instance (if any), as ``user``.
* ``is_admin`` flag indicating whether user has the Administrator role.
* ``is_root`` flag indicating whether user is currently elevated to root.
* A shortcut method for permission checking, as ``has_perm()``.
"""
request = event.request
rattail_config = request.registry.settings.get('rattail_config')
# TODO: why would this ever be null?
if rattail_config:
request.rattail_config = rattail_config
def user(request):
user = None
uuid = request.authenticated_userid
if uuid:
model = request.rattail_config.get_model()
user = Session.get(model.User, uuid)
if user:
Session().set_continuum_user(user)
return user
request.set_property(user, reify=True)
# assign client IP address to the session, for sake of versioning
Session().continuum_remote_addr = request.client_addr
request.is_admin = bool(request.user) and request.user.is_admin()
request.is_root = request.is_admin and request.session.get('is_root', False)
# TODO: why would this ever be null?
if rattail_config:
app = rattail_config.get_app()
auth = app.get_auth_handler()
request.tailbone_cached_permissions = auth.get_permissions(
Session(), request.user)
def has_perm(name):
if name in request.tailbone_cached_permissions:
return True
return request.is_root
request.has_perm = has_perm
def has_any_perm(*names):
for name in names:
if has_perm(name):
return True
return False
request.has_any_perm = has_any_perm
def before_render(event):
"""
Adds goodies to the global template renderer context.
"""
request = event.get('request') or threadlocal.get_current_request()
rattail_config = request.rattail_config
renderer_globals = event
renderer_globals['rattail_app'] = request.rattail_config.get_app()
renderer_globals['app_title'] = request.rattail_config.app_title()
renderer_globals['h'] = helpers
renderer_globals['url'] = request.route_url
renderer_globals['rattail'] = rattail
renderer_globals['tailbone'] = tailbone
renderer_globals['model'] = request.rattail_config.get_model()
renderer_globals['enum'] = request.rattail_config.get_enum()
renderer_globals['six'] = six
renderer_globals['json'] = json
renderer_globals['datetime'] = datetime
renderer_globals['colander'] = colander
renderer_globals['deform'] = deform
renderer_globals['csrf_header_name'] = csrf_header_name(request.rattail_config)
# theme - we only want do this for classic web app, *not* API
# TODO: so, clearly we need a better way to distinguish the two
if 'tailbone.theme' in request.registry.settings:
renderer_globals['theme'] = request.registry.settings['tailbone.theme']
# note, this is just a global flag; user still needs permission to see picker
expose_picker = request.rattail_config.getbool('tailbone', 'themes.expose_picker',
default=False)
renderer_globals['expose_theme_picker'] = expose_picker
if expose_picker:
# tailbone's config extension provides a default theme selection,
# so the default we specify here *probably* should not matter
available = request.rattail_config.getlist('tailbone', 'themes',
default=['falafel'])
if 'default' not in available:
available.insert(0, 'default')
options = [tags.Option(theme) for theme in available]
renderer_globals['theme_picker_options'] = options
# heck while we're assuming the classic web app here...
# (we don't want this to happen for the API either!)
# TODO: just..awful *shrug*
# note that we assume "simple" menus nowadays
if request.rattail_config.getbool('tailbone', 'menus.simple', default=True):
renderer_globals['menus'] = make_simple_menus(request)
# TODO: ugh, same deal here
renderer_globals['messaging_enabled'] = request.rattail_config.getbool(
'tailbone', 'messaging.enabled', default=False)
# background color may be set per-request, by some apps
if hasattr(request, 'background_color') and request.background_color:
renderer_globals['background_color'] = request.background_color
else: # otherwise we use the one from config
renderer_globals['background_color'] = request.rattail_config.get(
'tailbone', 'background_color')
# TODO: remove this hack once nothing references it
renderer_globals['buefy_0_8'] = False
# maybe set custom stylesheet
css = None
if request.user:
css = request.rattail_config.get('tailbone.{}'.format(request.user.uuid),
'buefy_css')
if not css:
css = request.rattail_config.get('tailbone', 'theme.falafel.buefy_css')
renderer_globals['buefy_css'] = css
# add global search data for quick access
renderer_globals['global_search_data'] = get_global_search_options(request)
# here we globally declare widths for grid filter pseudo-columns
widths = request.rattail_config.get('tailbone', 'grids.filters.column_widths')
if widths:
widths = widths.split(';')
if len(widths) < 2:
widths = None
if not widths:
widths = ['15em', '15em']
renderer_globals['filter_fieldname_width'] = widths[0]
renderer_globals['filter_verb_width'] = widths[1]
# declare global support for websockets, or lack thereof
renderer_globals['expose_websockets'] = should_expose_websockets(rattail_config)
def add_inbox_count(event):
"""
Adds the current user's inbox message count to the global renderer context.
Note that this is not enabled by default; to turn it on you must do this:
config.add_subscriber('tailbone.subscribers.add_inbox_count', 'pyramid.events.BeforeRender')
"""
request = event.get('request') or threadlocal.get_current_request()
if request.user:
renderer_globals = event
enum = request.rattail_config.get_enum()
model = request.rattail_config.get_model()
renderer_globals['inbox_count'] = Session.query(model.Message)\
.outerjoin(model.MessageRecipient)\
.filter(model.MessageRecipient.recipient == Session.merge(request.user))\
.filter(model.MessageRecipient.status == enum.MESSAGE_STATUS_INBOX)\
.count()
def context_found(event):
"""
Attach some more goodies to the request object:
The following is attached to the request:
* ``get_referrer()`` function
* ``get_session_timeout()`` function
"""
request = event.request
def get_referrer(default=None, **kwargs):
if request.params.get('referrer'):
return request.params['referrer']
if request.session.get('referrer'):
return request.session.pop('referrer')
referrer = request.referrer
if (not referrer or referrer == request.current_route_url()
or not referrer.startswith(request.host_url)):
if default:
referrer = default
else:
referrer = request.route_url('home')
return referrer
request.get_referrer = get_referrer
def get_session_timeout():
"""
Returns the timeout in effect for the current session
"""
return request.session.get('_timeout')
request.get_session_timeout = get_session_timeout
def includeme(config):
config.add_subscriber(new_request, 'pyramid.events.NewRequest')
config.add_subscriber(before_render, 'pyramid.events.BeforeRender')
config.add_subscriber(context_found, 'pyramid.events.ContextFound')