##// END OF EJS Templates
data-grid-app: added universal function for extracting ordering....
data-grid-app: added universal function for extracting ordering. - now supports format a.b - supports passing a ref column e.g User.username, get's handy for joined querie

File last commit:

r2024:1069b5cc merge default
r2040:a04ecb8d default
Show More
base.py
617 lines | 21.7 KiB | text/x-python | PythonLexer
project: added all source files and assets
r1 # -*- coding: utf-8 -*-
license: updated copyright year to 2017
r1271 # Copyright (C) 2010-2017 RhodeCode GmbH
project: added all source files and assets
r1 #
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program 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 Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
"""
The base Controller API
Provides the BaseController class for subclassing. And usage in different
controllers
"""
import logging
import socket
core: use a custom filter for rendering all mako templates....
r1949 import markupsafe
project: added all source files and assets
r1 import ipaddress
base: add pyramid_request to tmplContext
r765 import pyramid.threadlocal
project: added all source files and assets
r1
from paste.auth.basic import AuthBasicAuthenticator
from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
import rhodecode
from rhodecode.authentication.base import VCS_TYPE
from rhodecode.lib import auth, utils2
from rhodecode.lib import helpers as h
from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
from rhodecode.lib.exceptions import UserCreationError
from rhodecode.lib.utils import (
get_repo_slug, set_rhodecode_config, password_changed,
get_enabled_hook_classes)
from rhodecode.lib.utils2 import (
str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
from rhodecode.model import meta
comments: add comments type into comments.
r1324 from rhodecode.model.db import Repository, User, ChangesetComment
project: added all source files and assets
r1 from rhodecode.model.notification import NotificationModel
from rhodecode.model.scm import ScmModel
from rhodecode.model.settings import VcsSettingsModel, SettingsModel
repositories: rewrote whole admin section to pyramid....
r2014 # NOTE(marcink): remove after base controller is no longer required
from pylons.controllers import WSGIController
from pylons.i18n import translation
project: added all source files and assets
r1
log = logging.getLogger(__name__)
translation: unified usage of pluralize function ungettext....
r1945 # hack to make the migration to pyramid easier
def render(template_name, extra_vars=None, cache_key=None,
cache_type=None, cache_expire=None):
"""Render a template with Mako
Accepts the cache options ``cache_key``, ``cache_type``, and
``cache_expire``.
"""
repositories: rewrote whole admin section to pyramid....
r2014 from pylons.templating import literal
from pylons.templating import cached_template, pylons_globals
translation: unified usage of pluralize function ungettext....
r1945 # Create a render callable for the cache function
def render_template():
# Pull in extra vars if needed
globs = extra_vars or {}
# Second, get the globals
globs.update(pylons_globals())
globs['_ungettext'] = globs['ungettext']
# Grab a template reference
template = globs['app_globals'].mako_lookup.get_template(template_name)
return literal(template.render_unicode(**globs))
return cached_template(template_name, render_template, cache_key=cache_key,
cache_type=cache_type, cache_expire=cache_expire)
project: added all source files and assets
r1 def _filter_proxy(ip):
"""
Passed in IP addresses in HEADERS can be in a special format of multiple
ips. Those comma separated IPs are passed from various proxies in the
chain of request processing. The left-most being the original client.
We only care about the first IP which came from the org. client.
:param ip: ip string from headers
"""
if ',' in ip:
_ips = ip.split(',')
_first_ip = _ips[0].strip()
log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
return _first_ip
return ip
def _filter_port(ip):
"""
Removes a port from ip, there are 4 main cases to handle here.
- ipv4 eg. 127.0.0.1
- ipv6 eg. ::1
- ipv4+port eg. 127.0.0.1:8080
- ipv6+port eg. [::1]:8080
:param ip:
"""
def is_ipv6(ip_addr):
if hasattr(socket, 'inet_pton'):
try:
socket.inet_pton(socket.AF_INET6, ip_addr)
except socket.error:
return False
else:
# fallback to ipaddress
try:
dependencies: bumped pyramid-debugtoolbar to 4.3.1
r1907 ipaddress.IPv6Address(safe_unicode(ip_addr))
project: added all source files and assets
r1 except Exception:
return False
return True
if ':' not in ip: # must be ipv4 pure ip
return ip
if '[' in ip and ']' in ip: # ipv6 with port
return ip.split(']')[0][1:].lower()
# must be ipv6 or ipv4 with port
if is_ipv6(ip):
return ip
else:
ip, _port = ip.split(':')[:2] # means ipv4+port
return ip
def get_ip_addr(environ):
proxy_key = 'HTTP_X_REAL_IP'
proxy_key2 = 'HTTP_X_FORWARDED_FOR'
def_key = 'REMOTE_ADDR'
_filters = lambda x: _filter_port(_filter_proxy(x))
ip = environ.get(proxy_key)
if ip:
return _filters(ip)
ip = environ.get(proxy_key2)
if ip:
return _filters(ip)
ip = environ.get(def_key, '0.0.0.0')
return _filters(ip)
def get_server_ip_addr(environ, log_errors=True):
hostname = environ.get('SERVER_NAME')
try:
return socket.gethostbyname(hostname)
except Exception as e:
if log_errors:
# in some cases this lookup is not possible, and we don't want to
# make it an exception in logs
log.exception('Could not retrieve server ip address: %s', e)
return hostname
def get_server_port(environ):
return environ.get('SERVER_PORT')
def get_access_path(environ):
path = environ.get('PATH_INFO')
org_req = environ.get('pylons.original_request')
if org_req:
path = org_req.environ.get('PATH_INFO')
return path
hooks: expose user agent in the variables submitted to pull/push hooks.
r1710 def get_user_agent(environ):
return environ.get('HTTP_USER_AGENT')
project: added all source files and assets
r1 def vcs_operation_context(
Martin Bornhold
vcs: Add flag to indicate if repository is a shadow repository.
r899 environ, repo_name, username, action, scm, check_locking=True,
is_shadow_repo=False):
project: added all source files and assets
r1 """
Generate the context for a vcs operation, e.g. push or pull.
This context is passed over the layers so that hooks triggered by the
vcs operation know details like the user, the user's IP address etc.
:param check_locking: Allows to switch of the computation of the locking
data. This serves mainly the need of the simplevcs middleware to be
able to disable this for certain operations.
"""
# Tri-state value: False: unlock, None: nothing, True: lock
make_lock = None
locked_by = [None, None, None]
is_anonymous = username == User.DEFAULT_USER
if not is_anonymous and check_locking:
log.debug('Checking locking on repository "%s"', repo_name)
user = User.get_by_username(username)
repo = Repository.get_by_repo_name(repo_name)
make_lock, __, locked_by = repo.get_locking_state(
action, user.user_id)
settings_model = VcsSettingsModel(repo=repo_name)
ui_settings = settings_model.get_ui_settings()
extras = {
'ip': get_ip_addr(environ),
'username': username,
'action': action,
'repository': repo_name,
'scm': scm,
'config': rhodecode.CONFIG['__file__'],
'make_lock': make_lock,
'locked_by': locked_by,
'server_url': utils2.get_server_url(environ),
hooks: expose user agent in the variables submitted to pull/push hooks.
r1710 'user_agent': get_user_agent(environ),
project: added all source files and assets
r1 'hooks': get_enabled_hook_classes(ui_settings),
Martin Bornhold
vcs: Add flag to indicate if repository is a shadow repository.
r899 'is_shadow_repo': is_shadow_repo,
project: added all source files and assets
r1 }
return extras
class BasicAuth(AuthBasicAuthenticator):
Martin Bornhold
vcs: Pass registry to vcs for user authentication....
r591 def __init__(self, realm, authfunc, registry, auth_http_code=None,
authentication: enabled authentication with auth_token and repository scope....
r1510 initial_call_detection=False, acl_repo_name=None):
project: added all source files and assets
r1 self.realm = realm
self.initial_call = initial_call_detection
self.authfunc = authfunc
Martin Bornhold
vcs: Pass registry to vcs for user authentication....
r591 self.registry = registry
authentication: enabled authentication with auth_token and repository scope....
r1510 self.acl_repo_name = acl_repo_name
project: added all source files and assets
r1 self._rc_auth_http_code = auth_http_code
def _get_response_from_code(self, http_code):
try:
return get_exception(safe_int(http_code))
except Exception:
log.exception('Failed to fetch response for code %s' % http_code)
return HTTPForbidden
def build_authentication(self):
head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
if self._rc_auth_http_code and not self.initial_call:
# return alternative HTTP code if alternative http return code
# is specified in RhodeCode config, but ONLY if it's not the
# FIRST call
custom_response_klass = self._get_response_from_code(
self._rc_auth_http_code)
return custom_response_klass(headers=head)
return HTTPUnauthorized(headers=head)
def authenticate(self, environ):
authorization = AUTHORIZATION(environ)
if not authorization:
return self.build_authentication()
(authmeth, auth) = authorization.split(' ', 1)
if 'basic' != authmeth.lower():
return self.build_authentication()
auth = auth.strip().decode('base64')
_parts = auth.split(':', 1)
if len(_parts) == 2:
username, password = _parts
if self.authfunc(
Martin Bornhold
vcs: Pass registry to vcs for user authentication....
r591 username, password, environ, VCS_TYPE,
authentication: enabled authentication with auth_token and repository scope....
r1510 registry=self.registry, acl_repo_name=self.acl_repo_name):
project: added all source files and assets
r1 return username
if username and password:
# we mark that we actually executed authentication once, at
# that point we can use the alternative auth code
self.initial_call = False
return self.build_authentication()
__call__ = authenticate
core: removed usage of global pylons config in base lib.
r2000 def calculate_version_hash(config):
tests: remove usage of pylons context var in home app test.
r1893 return md5(
config.get('beaker.session.secret', '') +
rhodecode.__version__)[:8]
pyramid: move language extraction into a seperate method.
r1904 def get_current_lang(request):
# NOTE(marcink): remove after pyramid move
try:
return translation.get_lang()[0]
except:
pass
pyramid: fix problem with default language for users without any explicit one set.
r1919 return getattr(request, '_LOCALE_', request.locale_name)
pyramid: move language extraction into a seperate method.
r1904
core: always attach pyramid context into request...
r1896 def attach_context_attributes(context, request, user_id):
js-template-context: made the context pyramid and pylons compatible....
r400 """
Attach variables into template context called `c`, please note that
request could be pylons or pyramid request in here.
"""
core: removed usage of global pylons config in base lib.
r2000 # NOTE(marcink): remove check after pyramid migration
if hasattr(request, 'registry'):
config = request.registry.settings
else:
from pylons import config
dependencies: bumped pyramid-debugtoolbar to 4.3.1
r1907
settings: use cached settings in few places we only often use it for reading.
r260 rc_config = SettingsModel().get_all_settings(cache=True)
project: added all source files and assets
r1
context.rhodecode_version = rhodecode.__version__
context.rhodecode_edition = config.get('rhodecode.edition')
# unique secret + version does not leak the version but keep consistency
core: removed usage of global pylons config in base lib.
r2000 context.rhodecode_version_hash = calculate_version_hash(config)
project: added all source files and assets
r1
# Default language set for the incoming request
pyramid: move language extraction into a seperate method.
r1904 context.language = get_current_lang(request)
project: added all source files and assets
r1
# Visual options
context.visual = AttributeDict({})
vcs: moved svn proxy settings into vcs related settings...
r754 # DB stored Visual Items
project: added all source files and assets
r1 context.visual.show_public_icon = str2bool(
rc_config.get('rhodecode_show_public_icon'))
context.visual.show_private_icon = str2bool(
rc_config.get('rhodecode_show_private_icon'))
context.visual.stylify_metatags = str2bool(
rc_config.get('rhodecode_stylify_metatags'))
context.visual.dashboard_items = safe_int(
rc_config.get('rhodecode_dashboard_items', 100))
context.visual.admin_grid_items = safe_int(
rc_config.get('rhodecode_admin_grid_items', 100))
context.visual.repository_fields = str2bool(
rc_config.get('rhodecode_repository_fields'))
context.visual.show_version = str2bool(
rc_config.get('rhodecode_show_version'))
context.visual.use_gravatar = str2bool(
rc_config.get('rhodecode_use_gravatar'))
context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
context.visual.default_renderer = rc_config.get(
'rhodecode_markup_renderer', 'rst')
comments: add comments type into comments.
r1324 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
project: added all source files and assets
r1 context.visual.rhodecode_support_url = \
routing: switched static redirection links to pyramid....
r1679 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
project: added all source files and assets
r1
changelog: ported to pyramid views.
r1931 context.visual.affected_files_cut_off = 60
project: added all source files and assets
r1 context.pre_code = rc_config.get('rhodecode_pre_code')
context.post_code = rc_config.get('rhodecode_post_code')
context.rhodecode_name = rc_config.get('rhodecode_title')
context.default_encodings = aslist(config.get('default_encoding'), sep=',')
# if we have specified default_encoding in the request, it has more
# priority
if request.GET.get('default_encoding'):
context.default_encodings.insert(0, request.GET.get('default_encoding'))
context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
# INI stored
context.labs_active = str2bool(
config.get('labs_settings_active', 'false'))
context.visual.allow_repo_location_change = str2bool(
config.get('allow_repo_location_change', True))
context.visual.allow_custom_hooks_settings = str2bool(
config.get('allow_custom_hooks_settings', True))
context.debug_style = str2bool(config.get('debug_style', False))
context.rhodecode_instanceid = config.get('instance_id')
pull-requests: prepare the migration of pull request to pyramid....
r1813 context.visual.cut_off_limit_diff = safe_int(
config.get('cut_off_limit_diff'))
context.visual.cut_off_limit_file = safe_int(
config.get('cut_off_limit_file'))
project: added all source files and assets
r1 # AppEnlight
context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
context.appenlight_api_public_key = config.get(
'appenlight.api_public_key', '')
context.appenlight_server_url = config.get('appenlight.server_url', '')
js-template-context: made the context pyramid and pylons compatible....
r400 # JS template context
context.template_context = {
'repo_name': None,
'repo_type': None,
'repo_landing_commit': None,
'rhodecode_user': {
'username': None,
'email': None,
notifications: support real-time notifications with websockets via channelstream
r526 'notification_status': False
js-template-context: made the context pyramid and pylons compatible....
r400 },
'visual': {
'default_renderer': None
},
'commit_data': {
'commit_id': None
},
'pull_request_data': {'pull_request_id': None},
'timeago': {
'refresh_time': 120 * 1000,
'cutoff_limit': 1000 * 60 * 60 * 24 * 7
},
'pyramid_dispatch': {
},
'extra': {'plugins': {}}
}
project: added all source files and assets
r1 # END CONFIG VARS
# TODO: This dosn't work when called from pylons compatibility tween.
# Fix this and remove it from base controller.
# context.repo_name = get_repo_slug(request) # can be empty
dan
diffs: move diffmode to template global context and add it to session...
r1137 diffmode = 'sideside'
if request.GET.get('diffmode'):
if request.GET['diffmode'] == 'unified':
diffmode = 'unified'
elif request.session.get('diffmode'):
diffmode = request.session['diffmode']
context.diffmode = diffmode
if request.session.get('diffmode') != diffmode:
request.session['diffmode'] = diffmode
dependencies: bumped pyramid to 1.9 webob to 1.7.3 and webtest to 2.0.27...
r1906 context.csrf_token = auth.get_csrf_token(session=request.session)
project: added all source files and assets
r1 context.backends = rhodecode.BACKENDS.keys()
context.backends.sort()
core: improve attach context attribute to rely strictly on already provided user.
r1776 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
dependencies: bumped pyramid to 1.9 webob to 1.7.3 and webtest to 2.0.27...
r1906
# NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
# given request will ALWAYS be pyramid one
pyramid_request = pyramid.threadlocal.get_current_request()
context.pyramid_request = pyramid_request
# web case
if hasattr(pyramid_request, 'user'):
context.auth_user = pyramid_request.user
context.rhodecode_user = pyramid_request.user
# api case
if hasattr(pyramid_request, 'rpc_user'):
context.auth_user = pyramid_request.rpc_user
context.rhodecode_user = pyramid_request.rpc_user
dan
api: attach the call context variables to request for later usage...
r1794
core: always attach pyramid context into request...
r1896 # attach the whole call context to the request
request.call_context = context
dan
api: attach the call context variables to request for later usage...
r1794
base: add pyramid_request to tmplContext
r765
pyramid: moved extraction of user into a seperate subscriber.
r1903 def get_auth_user(request):
environ = request.environ
session = request.session
project: added all source files and assets
r1
ip_addr = get_ip_addr(environ)
# make sure that we update permissions each time we call controller
_auth_token = (request.GET.get('auth_token', '') or
request.GET.get('api_key', ''))
if _auth_token:
code: added more logging, and some notes
r1300 # when using API_KEY we assume user exists, and
# doesn't need auth based on cookies.
project: added all source files and assets
r1 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
authenticated = False
else:
cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
try:
auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
ip_addr=ip_addr)
except UserCreationError as e:
h.flash(e, 'error')
# container auth or other auth functions that create users
# on the fly can throw this exception signaling that there's
# issue with user creation, explanation should be provided
# in Exception itself. We then create a simple blank
# AuthUser
auth_user = AuthUser(ip_addr=ip_addr)
if password_changed(auth_user, session):
session.invalidate()
code: added more logging, and some notes
r1300 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
project: added all source files and assets
r1 auth_user = AuthUser(ip_addr=ip_addr)
authenticated = cookie_store.get('is_authenticated')
if not auth_user.is_authenticated and auth_user.is_user_object:
# user is not authenticated and not empty
auth_user.set_authenticated(authenticated)
return auth_user
class BaseController(WSGIController):
def __before__(self):
"""
__before__ is called before controller methods and after __call__
"""
# on each call propagate settings calls into global settings.
core: removed usage of global pylons config in base lib.
r2000 from pylons import config
repositories: rewrote whole admin section to pyramid....
r2014 from pylons import tmpl_context as c, request, url
project: added all source files and assets
r1 set_rhodecode_config(config)
dependencies: bumped pyramid to 1.9 webob to 1.7.3 and webtest to 2.0.27...
r1906 attach_context_attributes(c, request, self._rhodecode_user.user_id)
project: added all source files and assets
r1
# TODO: Remove this when fixed in attach_context_attributes()
c.repo_name = get_repo_slug(request) # can be empty
self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
self.sa = meta.Session
self.scm_model = ScmModel(self.sa)
i18n: use consistent way of setting user language.
r1307 # set user language
user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
if user_lang:
project: added all source files and assets
r1 translation.set_lang(user_lang)
i18n: use consistent way of setting user language.
r1307 log.debug('set language to %s for user %s',
user_lang, self._rhodecode_user)
project: added all source files and assets
r1
def _dispatch_redirect(self, with_url, environ, start_response):
repositories: rewrote whole admin section to pyramid....
r2014 from webob.exc import HTTPFound
project: added all source files and assets
r1 resp = HTTPFound(with_url)
environ['SCRIPT_NAME'] = '' # handle prefix middleware
environ['PATH_INFO'] = with_url
return resp(environ, start_response)
def __call__(self, environ, start_response):
"""Invoke the Controller"""
# WSGIController.__call__ dispatches to the Controller method
# the request is routed to. This routing information is
# available in environ['pylons.routes_dict']
from rhodecode.lib import helpers as h
repositories: rewrote whole admin section to pyramid....
r2014 from pylons import tmpl_context as c, request, url
project: added all source files and assets
r1
# Provide the Pylons context to Pyramid's debugtoolbar if it asks
if environ.get('debugtoolbar.wants_pylons_context', False):
environ['debugtoolbar.pylons_context'] = c._current_obj()
_route_name = '.'.join([environ['pylons.routes_dict']['controller'],
environ['pylons.routes_dict']['action']])
settings: use cached settings in few places we only often use it for reading.
r260 self.rc_config = SettingsModel().get_all_settings(cache=True)
project: added all source files and assets
r1 self.ip_addr = get_ip_addr(environ)
# The rhodecode auth user is looked up and passed through the
# environ by the pylons compatibility tween in pyramid.
# So we can just grab it from there.
auth_user = environ['rc_auth_user']
# set globals for auth user
request.user = auth_user
dependencies: bumped pyramid to 1.9 webob to 1.7.3 and webtest to 2.0.27...
r1906 self._rhodecode_user = auth_user
project: added all source files and assets
r1
log.info('IP: %s User: %s accessed %s [%s]' % (
self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
_route_name)
)
user_obj = auth_user.get_instance()
if user_obj and user_obj.user_data.get('force_password_change'):
h.flash('You are required to change your password', 'warning',
ignore_duplicate=True)
pyramid: added checks for password change for authenticated users.
r1539 return self._dispatch_redirect(
url('my_account_password'), environ, start_response)
project: added all source files and assets
r1
return WSGIController.__call__(self, environ, start_response)
core: use a custom filter for rendering all mako templates....
r1949 def h_filter(s):
"""
Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
we wrap this with additional functionality that converts None to empty
strings
"""
if s is None:
return markupsafe.Markup()
return markupsafe.escape(s)
pyramid: ported pyramid routing for events
r2016 def add_events_routes(config):
"""
Adds routing that can be used in events. Because some events are triggered
outside of pyramid context, we need to bootstrap request with some
routing registered
"""
config.add_route(name='home', pattern='/')
config.add_route(name='repo_summary', pattern='/{repo_name}')
config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
config.add_route(name='pullrequest_show',
pattern='/{repo_name}/pull-request/{pull_request_id}')
config.add_route(name='pull_requests_global',
pattern='/pull-request/{pull_request_id}')
config.add_route(name='repo_commit',
pattern='/{repo_name}/changeset/{commit_id}')
config.add_route(name='repo_files',
pattern='/{repo_name}/files/{commit_id}/{f_path}')
events: make sure we propagate our dummy request with proper application_url....
r2017 def bootstrap_request(**kwargs):
pyramid: ported pyramid routing for events
r2016 import pyramid.testing
events: make sure we propagate our dummy request with proper application_url....
r2017 request = pyramid.testing.DummyRequest(**kwargs)
request.application_url = kwargs.pop('application_url', 'http://example.com')
request.host = kwargs.pop('host', 'example.com:80')
request.domain = kwargs.pop('domain', 'example.com')
pyramid: ported pyramid routing for events
r2016 config = pyramid.testing.setUp(request=request)
add_events_routes(config)