changeset 8809:4f0de9468da3

controllers: move controllers base class from lib/base to controllers TG quickstart put it in lib/base.py , but it fits better on the controllers layer as a base there. The contributing docs were a bit ahead of time ... but with a typo.
author Mads Kiilerich <mads@kiilerich.com>
date Fri, 13 Nov 2020 01:06:16 +0100
parents 9a2c0a067c6e
children 2d34e0499789
files docs/contributing.rst kallithea/config/middleware/simplegit.py kallithea/config/middleware/simplehg.py kallithea/config/middleware/wrapper.py kallithea/controllers/admin/admin.py kallithea/controllers/admin/auth_settings.py kallithea/controllers/admin/defaults.py kallithea/controllers/admin/gists.py kallithea/controllers/admin/my_account.py kallithea/controllers/admin/permissions.py kallithea/controllers/admin/repo_groups.py kallithea/controllers/admin/repos.py kallithea/controllers/admin/settings.py kallithea/controllers/admin/user_groups.py kallithea/controllers/admin/users.py kallithea/controllers/api/__init__.py kallithea/controllers/base.py kallithea/controllers/changelog.py kallithea/controllers/changeset.py kallithea/controllers/compare.py kallithea/controllers/error.py kallithea/controllers/feed.py kallithea/controllers/files.py kallithea/controllers/followers.py kallithea/controllers/forks.py kallithea/controllers/home.py kallithea/controllers/journal.py kallithea/controllers/login.py kallithea/controllers/pullrequests.py kallithea/controllers/root.py kallithea/controllers/search.py kallithea/controllers/summary.py kallithea/lib/base.py scripts/deps.py
diffstat 34 files changed, 848 insertions(+), 848 deletions(-) [+]
line wrap: on
line diff
--- a/docs/contributing.rst	Mon Dec 14 23:00:25 2020 +0100
+++ b/docs/contributing.rst	Fri Nov 13 01:06:16 2020 +0100
@@ -112,7 +112,7 @@
   everything and has a huge dependency chain, so it should not be used for
   anything else. TODO.
 
-``controlles/base.py``
+``controllers/base.py``
   The base class of controllers, with lots of model knowledge.
 
 ``lib/auth.py``
--- a/kallithea/config/middleware/simplegit.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/config/middleware/simplegit.py	Fri Nov 13 01:06:16 2020 +0100
@@ -32,8 +32,8 @@
 import re
 
 from kallithea.config.middleware.pygrack import make_wsgi_app
+from kallithea.controllers import base
 from kallithea.lib import hooks
-from kallithea.lib.base import BaseVCSController, get_path_info
 
 
 log = logging.getLogger(__name__)
@@ -48,13 +48,13 @@
 }
 
 
-class SimpleGit(BaseVCSController):
+class SimpleGit(base.BaseVCSController):
 
     scm_alias = 'git'
 
     @classmethod
     def parse_request(cls, environ):
-        path_info = get_path_info(environ)
+        path_info = base.get_path_info(environ)
         m = GIT_PROTO_PAT.match(path_info)
         if m is None:
             return None
--- a/kallithea/config/middleware/simplehg.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/config/middleware/simplehg.py	Fri Nov 13 01:06:16 2020 +0100
@@ -34,7 +34,7 @@
 
 import mercurial.hgweb
 
-from kallithea.lib.base import BaseVCSController, get_path_info
+from kallithea.controllers import base
 from kallithea.lib.utils import make_ui
 from kallithea.lib.utils2 import safe_bytes
 
@@ -91,7 +91,7 @@
     }
 
 
-class SimpleHg(BaseVCSController):
+class SimpleHg(base.BaseVCSController):
 
     scm_alias = 'hg'
 
@@ -100,7 +100,7 @@
         http_accept = environ.get('HTTP_ACCEPT', '')
         if not http_accept.startswith('application/mercurial'):
             return None
-        path_info = get_path_info(environ)
+        path_info = base.get_path_info(environ)
         if not path_info.startswith('/'): # it must!
             return None
 
--- a/kallithea/config/middleware/wrapper.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/config/middleware/wrapper.py	Fri Nov 13 01:06:16 2020 +0100
@@ -29,7 +29,7 @@
 import logging
 import time
 
-from kallithea.lib.base import get_ip_addr, get_path_info
+from kallithea.controllers import base
 
 
 log = logging.getLogger(__name__)
@@ -91,8 +91,8 @@
     def __call__(self, environ, start_response):
         meter = Meter(start_response)
         description = "Request from %s for %s" % (
-            get_ip_addr(environ),
-            get_path_info(environ),
+            base.get_ip_addr(environ),
+            base.get_path_info(environ),
         )
         log.info("%s received", description)
         try:
--- a/kallithea/controllers/admin/admin.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/admin/admin.py	Fri Nov 13 01:06:16 2020 +0100
@@ -36,8 +36,8 @@
 from whoosh.qparser.dateparse import DateParserPlugin
 from whoosh.qparser.default import QueryParser
 
+from kallithea.controllers import base
 from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired
-from kallithea.lib.base import BaseController, render
 from kallithea.lib.indexers import JOURNAL_SCHEMA
 from kallithea.lib.page import Page
 from kallithea.lib.utils2 import remove_prefix, remove_suffix, safe_int
@@ -118,7 +118,7 @@
     return user_log
 
 
-class AdminController(BaseController):
+class AdminController(base.BaseController):
 
     @LoginRequired(allow_default_user=True)
     def _before(self, *args, **kwargs):
@@ -142,6 +142,6 @@
                            filter=c.search_term)
 
         if request.environ.get('HTTP_X_PARTIAL_XHR'):
-            return render('admin/admin_log.html')
+            return base.render('admin/admin_log.html')
 
-        return render('admin/admin.html')
+        return base.render('admin/admin.html')
--- a/kallithea/controllers/admin/auth_settings.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/admin/auth_settings.py	Fri Nov 13 01:06:16 2020 +0100
@@ -32,9 +32,9 @@
 from tg.i18n import ugettext as _
 from webob.exc import HTTPFound
 
+from kallithea.controllers import base
 from kallithea.lib import auth_modules, webutils
 from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired
-from kallithea.lib.base import BaseController, render
 from kallithea.lib.webutils import url
 from kallithea.model import db, meta
 from kallithea.model.forms import AuthSettingsForm
@@ -43,7 +43,7 @@
 log = logging.getLogger(__name__)
 
 
-class AuthSettingsController(BaseController):
+class AuthSettingsController(base.BaseController):
 
     @LoginRequired()
     @HasPermissionAnyDecorator('hg.admin')
@@ -86,7 +86,7 @@
 
         log.debug('defaults: %s', defaults)
         return formencode.htmlfill.render(
-            render('admin/auth/auth_settings.html'),
+            base.render('admin/auth/auth_settings.html'),
             defaults=c.defaults,
             errors=errors,
             prefix_error=False,
--- a/kallithea/controllers/admin/defaults.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/admin/defaults.py	Fri Nov 13 01:06:16 2020 +0100
@@ -34,9 +34,9 @@
 from tg.i18n import ugettext as _
 from webob.exc import HTTPFound
 
+from kallithea.controllers import base
 from kallithea.lib import webutils
 from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired
-from kallithea.lib.base import BaseController, render
 from kallithea.lib.webutils import url
 from kallithea.model import db, meta
 from kallithea.model.forms import DefaultsForm
@@ -45,7 +45,7 @@
 log = logging.getLogger(__name__)
 
 
-class DefaultsController(BaseController):
+class DefaultsController(base.BaseController):
 
     @LoginRequired()
     @HasPermissionAnyDecorator('hg.admin')
@@ -56,7 +56,7 @@
         defaults = db.Setting.get_default_repo_settings()
 
         return htmlfill.render(
-            render('admin/defaults/defaults.html'),
+            base.render('admin/defaults/defaults.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False
@@ -77,7 +77,7 @@
             defaults = errors.value
 
             return htmlfill.render(
-                render('admin/defaults/defaults.html'),
+                base.render('admin/defaults/defaults.html'),
                 defaults=defaults,
                 errors=errors.error_dict or {},
                 prefix_error=False,
--- a/kallithea/controllers/admin/gists.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/admin/gists.py	Fri Nov 13 01:06:16 2020 +0100
@@ -35,9 +35,9 @@
 from tg.i18n import ugettext as _
 from webob.exc import HTTPForbidden, HTTPFound, HTTPNotFound
 
+from kallithea.controllers import base
 from kallithea.lib import auth, webutils
 from kallithea.lib.auth import LoginRequired
-from kallithea.lib.base import BaseController, jsonify, render
 from kallithea.lib.page import Page
 from kallithea.lib.utils2 import safe_int, safe_str, time_to_datetime
 from kallithea.lib.vcs.exceptions import NodeNotChangedError, VCSError
@@ -50,7 +50,7 @@
 log = logging.getLogger(__name__)
 
 
-class GistsController(BaseController):
+class GistsController(base.BaseController):
 
     def __load_defaults(self, extra_values=None):
         c.lifetime_values = [
@@ -102,7 +102,7 @@
         p = safe_int(request.GET.get('page'), 1)
         c.gists_pager = Page(c.gists, page=p, items_per_page=10,
                              **url_params)
-        return render('admin/gists/index.html')
+        return base.render('admin/gists/index.html')
 
     @LoginRequired()
     def create(self):
@@ -134,7 +134,7 @@
             defaults = errors.value
 
             return formencode.htmlfill.render(
-                render('admin/gists/new.html'),
+                base.render('admin/gists/new.html'),
                 defaults=defaults,
                 errors=errors.error_dict or {},
                 prefix_error=False,
@@ -150,7 +150,7 @@
     @LoginRequired()
     def new(self, format='html'):
         self.__load_defaults()
-        return render('admin/gists/new.html')
+        return base.render('admin/gists/new.html')
 
     @LoginRequired()
     def delete(self, gist_id):
@@ -186,7 +186,7 @@
             )
             response.content_type = 'text/plain'
             return content
-        return render('admin/gists/show.html')
+        return base.render('admin/gists/show.html')
 
     @LoginRequired()
     def edit(self, gist_id, format='html'):
@@ -203,7 +203,7 @@
             raise HTTPNotFound()
 
         self.__load_defaults(extra_values=('0', _('Unmodified')))
-        rendered = render('admin/gists/edit.html')
+        rendered = base.render('admin/gists/edit.html')
 
         if request.POST:
             rpost = request.POST
@@ -248,7 +248,7 @@
         return rendered
 
     @LoginRequired()
-    @jsonify
+    @base.jsonify
     def check_revision(self, gist_id):
         c.gist = db.Gist.get_or_404(gist_id)
         last_rev = c.gist.scm_instance.get_changeset()
--- a/kallithea/controllers/admin/my_account.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/admin/my_account.py	Fri Nov 13 01:06:16 2020 +0100
@@ -35,9 +35,9 @@
 from tg.i18n import ugettext as _
 from webob.exc import HTTPFound
 
+from kallithea.controllers import base
 from kallithea.lib import auth_modules, webutils
 from kallithea.lib.auth import AuthUser, LoginRequired
-from kallithea.lib.base import BaseController, IfSshEnabled, render
 from kallithea.lib.utils2 import generate_api_key, safe_int
 from kallithea.lib.webutils import url
 from kallithea.model import db, meta
@@ -51,7 +51,7 @@
 log = logging.getLogger(__name__)
 
 
-class MyAccountController(BaseController):
+class MyAccountController(base.BaseController):
 
     @LoginRequired()
     def _before(self, *args, **kwargs):
@@ -116,7 +116,7 @@
 
             except formencode.Invalid as errors:
                 return htmlfill.render(
-                    render('admin/my_account/my_account.html'),
+                    base.render('admin/my_account/my_account.html'),
                     defaults=errors.value,
                     errors=errors.error_dict or {},
                     prefix_error=False,
@@ -129,7 +129,7 @@
         if update:
             raise HTTPFound(location='my_account')
         return htmlfill.render(
-            render('admin/my_account/my_account.html'),
+            base.render('admin/my_account/my_account.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -150,7 +150,7 @@
                 webutils.flash(_("Successfully updated password"), category='success')
             except formencode.Invalid as errors:
                 return htmlfill.render(
-                    render('admin/my_account/my_account.html'),
+                    base.render('admin/my_account/my_account.html'),
                     defaults=errors.value,
                     errors=errors.error_dict or {},
                     prefix_error=False,
@@ -160,7 +160,7 @@
                 log.error(traceback.format_exc())
                 webutils.flash(_('Error occurred during update of user password'),
                         category='error')
-        return render('admin/my_account/my_account.html')
+        return base.render('admin/my_account/my_account.html')
 
     def my_account_repos(self):
         c.active = 'repos'
@@ -168,7 +168,7 @@
 
         # data used to render the grid
         c.data = self._load_my_repos_data()
-        return render('admin/my_account/my_account.html')
+        return base.render('admin/my_account/my_account.html')
 
     def my_account_watched(self):
         c.active = 'watched'
@@ -176,14 +176,14 @@
 
         # data used to render the grid
         c.data = self._load_my_repos_data(watched=True)
-        return render('admin/my_account/my_account.html')
+        return base.render('admin/my_account/my_account.html')
 
     def my_account_perms(self):
         c.active = 'perms'
         self.__load_data()
         c.perm_user = AuthUser(user_id=request.authuser.user_id)
 
-        return render('admin/my_account/my_account.html')
+        return base.render('admin/my_account/my_account.html')
 
     def my_account_emails(self):
         c.active = 'emails'
@@ -191,7 +191,7 @@
 
         c.user_email_map = db.UserEmailMap.query() \
             .filter(db.UserEmailMap.user == c.user).all()
-        return render('admin/my_account/my_account.html')
+        return base.render('admin/my_account/my_account.html')
 
     def my_account_emails_add(self):
         email = request.POST.get('new_email')
@@ -231,7 +231,7 @@
         c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
         c.user_api_keys = ApiKeyModel().get_api_keys(request.authuser.user_id,
                                                      show_expired=show_expired)
-        return render('admin/my_account/my_account.html')
+        return base.render('admin/my_account/my_account.html')
 
     def my_account_api_keys_add(self):
         lifetime = safe_int(request.POST.get('lifetime'), -1)
@@ -255,14 +255,14 @@
 
         raise HTTPFound(location=url('my_account_api_keys'))
 
-    @IfSshEnabled
+    @base.IfSshEnabled
     def my_account_ssh_keys(self):
         c.active = 'ssh_keys'
         self.__load_data()
         c.user_ssh_keys = SshKeyModel().get_ssh_keys(request.authuser.user_id)
-        return render('admin/my_account/my_account.html')
+        return base.render('admin/my_account/my_account.html')
 
-    @IfSshEnabled
+    @base.IfSshEnabled
     def my_account_ssh_keys_add(self):
         description = request.POST.get('description')
         public_key = request.POST.get('public_key')
@@ -276,7 +276,7 @@
             webutils.flash(e.args[0], category='error')
         raise HTTPFound(location=url('my_account_ssh_keys'))
 
-    @IfSshEnabled
+    @base.IfSshEnabled
     def my_account_ssh_keys_delete(self):
         fingerprint = request.POST.get('del_public_key_fingerprint')
         try:
--- a/kallithea/controllers/admin/permissions.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/admin/permissions.py	Fri Nov 13 01:06:16 2020 +0100
@@ -36,9 +36,9 @@
 from tg.i18n import ugettext as _
 from webob.exc import HTTPFound
 
+from kallithea.controllers import base
 from kallithea.lib import webutils
 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator, LoginRequired
-from kallithea.lib.base import BaseController, render
 from kallithea.lib.webutils import url
 from kallithea.model import db, meta
 from kallithea.model.forms import DefaultPermissionsForm
@@ -48,7 +48,7 @@
 log = logging.getLogger(__name__)
 
 
-class PermissionsController(BaseController):
+class PermissionsController(base.BaseController):
 
     @LoginRequired()
     @HasPermissionAnyDecorator('hg.admin')
@@ -120,7 +120,7 @@
                 defaults = errors.value
 
                 return htmlfill.render(
-                    render('admin/permissions/permissions.html'),
+                    base.render('admin/permissions/permissions.html'),
                     defaults=defaults,
                     errors=errors.error_dict or {},
                     prefix_error=False,
@@ -162,7 +162,7 @@
                 defaults['default_fork'] = p.permission.permission_name
 
         return htmlfill.render(
-            render('admin/permissions/permissions.html'),
+            base.render('admin/permissions/permissions.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -173,10 +173,10 @@
         c.user_ip_map = db.UserIpMap.query() \
                         .filter(db.UserIpMap.user == c.user).all()
 
-        return render('admin/permissions/permissions.html')
+        return base.render('admin/permissions/permissions.html')
 
     def permission_perms(self):
         c.active = 'perms'
         c.user = db.User.get_default_user()
         c.perm_user = AuthUser(dbuser=c.user)
-        return render('admin/permissions/permissions.html')
+        return base.render('admin/permissions/permissions.html')
--- a/kallithea/controllers/admin/repo_groups.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/admin/repo_groups.py	Fri Nov 13 01:06:16 2020 +0100
@@ -37,9 +37,9 @@
 from webob.exc import HTTPForbidden, HTTPFound, HTTPInternalServerError, HTTPNotFound
 
 import kallithea.lib.helpers as h
+from kallithea.controllers import base
 from kallithea.lib import webutils
 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel, HasRepoGroupPermissionLevelDecorator, LoginRequired
-from kallithea.lib.base import BaseController, render
 from kallithea.lib.utils2 import safe_int
 from kallithea.lib.webutils import url
 from kallithea.model import db, meta
@@ -52,7 +52,7 @@
 log = logging.getLogger(__name__)
 
 
-class RepoGroupsController(BaseController):
+class RepoGroupsController(base.BaseController):
 
     @LoginRequired(allow_default_user=True)
     def _before(self, *args, **kwargs):
@@ -132,7 +132,7 @@
             "records": repo_groups_data
         }
 
-        return render('admin/repo_groups/repo_groups.html')
+        return base.render('admin/repo_groups/repo_groups.html')
 
     def create(self):
         self.__load_defaults()
@@ -154,7 +154,7 @@
             # TODO: in future action_logger(, '', '', '')
         except formencode.Invalid as errors:
             return htmlfill.render(
-                render('admin/repo_groups/repo_group_add.html'),
+                base.render('admin/repo_groups/repo_group_add.html'),
                 defaults=errors.value,
                 errors=errors.error_dict or {},
                 prefix_error=False,
@@ -190,7 +190,7 @@
 
         self.__load_defaults()
         return htmlfill.render(
-            render('admin/repo_groups/repo_group_add.html'),
+            base.render('admin/repo_groups/repo_group_add.html'),
             defaults={'parent_group_id': parent_group_id},
             errors={},
             prefix_error=False,
@@ -230,7 +230,7 @@
         except formencode.Invalid as errors:
             c.active = 'settings'
             return htmlfill.render(
-                render('admin/repo_groups/repo_group_edit.html'),
+                base.render('admin/repo_groups/repo_group_edit.html'),
                 defaults=errors.value,
                 errors=errors.error_dict or {},
                 prefix_error=False,
@@ -298,7 +298,7 @@
                                                repo_groups_list=repo_groups_list,
                                                short_name=True)
 
-        return render('admin/repo_groups/repo_group_show.html')
+        return base.render('admin/repo_groups/repo_group_show.html')
 
     @HasRepoGroupPermissionLevelDecorator('admin')
     def edit(self, group_name):
@@ -310,7 +310,7 @@
         defaults = self.__load_data(c.repo_group.group_id)
 
         return htmlfill.render(
-            render('admin/repo_groups/repo_group_edit.html'),
+            base.render('admin/repo_groups/repo_group_edit.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False
@@ -321,7 +321,7 @@
         c.active = 'advanced'
         c.repo_group = db.RepoGroup.guess_instance(group_name)
 
-        return render('admin/repo_groups/repo_group_edit.html')
+        return base.render('admin/repo_groups/repo_group_edit.html')
 
     @HasRepoGroupPermissionLevelDecorator('admin')
     def edit_repo_group_perms(self, group_name):
@@ -331,7 +331,7 @@
         defaults = self.__load_data(c.repo_group.group_id)
 
         return htmlfill.render(
-            render('admin/repo_groups/repo_group_edit.html'),
+            base.render('admin/repo_groups/repo_group_edit.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False
--- a/kallithea/controllers/admin/repos.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/admin/repos.py	Fri Nov 13 01:06:16 2020 +0100
@@ -36,9 +36,9 @@
 from webob.exc import HTTPForbidden, HTTPFound, HTTPInternalServerError, HTTPNotFound
 
 import kallithea
+from kallithea.controllers import base
 from kallithea.lib import webutils
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired, NotAnonymous
-from kallithea.lib.base import BaseRepoController, jsonify, render
 from kallithea.lib.exceptions import AttachedForksError
 from kallithea.lib.utils2 import safe_int
 from kallithea.lib.vcs import RepositoryError
@@ -52,7 +52,7 @@
 log = logging.getLogger(__name__)
 
 
-class ReposController(BaseRepoController):
+class ReposController(base.BaseRepoController):
 
     @LoginRequired(allow_default_user=True)
     def _before(self, *args, **kwargs):
@@ -93,7 +93,7 @@
         # data used to render the grid
         c.data = repos_data
 
-        return render('admin/repos/repos.html')
+        return base.render('admin/repos/repos.html')
 
     @NotAnonymous()
     def create(self):
@@ -106,7 +106,7 @@
         except formencode.Invalid as errors:
             log.info(errors)
             return htmlfill.render(
-                render('admin/repos/repo_add.html'),
+                base.render('admin/repos/repo_add.html'),
                 defaults=errors.value,
                 errors=errors.error_dict or {},
                 prefix_error=False,
@@ -148,7 +148,7 @@
         defaults.update({'repo_group': parent_group})
 
         return htmlfill.render(
-            render('admin/repos/repo_add.html'),
+            base.render('admin/repos/repo_add.html'),
             defaults=defaults,
             errors={},
             prefix_error=False,
@@ -161,10 +161,10 @@
         c.task_id = request.GET.get('task_id')
         if not c.repo:
             raise HTTPNotFound()
-        return render('admin/repos/repo_creating.html')
+        return base.render('admin/repos/repo_creating.html')
 
     @LoginRequired()
-    @jsonify
+    @base.jsonify
     def repo_check(self, repo_name):
         c.repo = repo_name
         task_id = request.GET.get('task_id')
@@ -230,7 +230,7 @@
             defaults = self.__load_data()
             defaults.update(errors.value)
             return htmlfill.render(
-                render('admin/repos/repo_edit.html'),
+                base.render('admin/repos/repo_edit.html'),
                 defaults=defaults,
                 errors=errors.error_dict or {},
                 prefix_error=False,
@@ -286,7 +286,7 @@
             .filter(db.RepositoryField.repository == c.repo_info).all()
         c.active = 'settings'
         return htmlfill.render(
-            render('admin/repos/repo_edit.html'),
+            base.render('admin/repos/repo_edit.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -298,7 +298,7 @@
         defaults = RepoModel()._get_defaults(repo_name)
 
         return htmlfill.render(
-            render('admin/repos/repo_edit.html'),
+            base.render('admin/repos/repo_edit.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -355,7 +355,7 @@
         if request.POST:
 
             raise HTTPFound(location=url('repo_edit_fields'))
-        return render('admin/repos/repo_edit.html')
+        return base.render('admin/repos/repo_edit.html')
 
     @HasRepoPermissionLevelDecorator('admin')
     def create_repo_field(self, repo_name):
@@ -413,7 +413,7 @@
         if request.POST:
             raise HTTPFound(location=url('repo_edit_advanced'))
         return htmlfill.render(
-            render('admin/repos/repo_edit.html'),
+            base.render('admin/repos/repo_edit.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -478,7 +478,7 @@
                 webutils.flash(_('An error occurred during pull from remote location'),
                         category='error')
             raise HTTPFound(location=url('edit_repo_remote', repo_name=c.repo_name))
-        return render('admin/repos/repo_edit.html')
+        return base.render('admin/repos/repo_edit.html')
 
     @HasRepoPermissionLevelDecorator('admin')
     def edit_statistics(self, repo_name):
@@ -510,4 +510,4 @@
                         category='error')
             raise HTTPFound(location=url('edit_repo_statistics', repo_name=c.repo_name))
 
-        return render('admin/repos/repo_edit.html')
+        return base.render('admin/repos/repo_edit.html')
--- a/kallithea/controllers/admin/settings.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/admin/settings.py	Fri Nov 13 01:06:16 2020 +0100
@@ -36,9 +36,9 @@
 from webob.exc import HTTPFound
 
 import kallithea
+from kallithea.controllers import base
 from kallithea.lib import webutils
 from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired
-from kallithea.lib.base import BaseController, render
 from kallithea.lib.utils import repo2db_mapper, set_app_settings
 from kallithea.lib.utils2 import safe_str
 from kallithea.lib.vcs import VCSError
@@ -52,7 +52,7 @@
 log = logging.getLogger(__name__)
 
 
-class SettingsController(BaseController):
+class SettingsController(base.BaseController):
 
     @LoginRequired(allow_default_user=True)
     def _before(self, *args, **kwargs):
@@ -85,7 +85,7 @@
                 form_result = application_form.to_python(dict(request.POST))
             except formencode.Invalid as errors:
                 return htmlfill.render(
-                     render('admin/settings/settings.html'),
+                     base.render('admin/settings/settings.html'),
                      defaults=errors.value,
                      errors=errors.error_dict or {},
                      prefix_error=False,
@@ -124,7 +124,7 @@
         defaults.update(self._get_hg_ui_settings())
 
         return htmlfill.render(
-            render('admin/settings/settings.html'),
+            base.render('admin/settings/settings.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -172,7 +172,7 @@
         defaults.update(self._get_hg_ui_settings())
 
         return htmlfill.render(
-            render('admin/settings/settings.html'),
+            base.render('admin/settings/settings.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -186,7 +186,7 @@
                 form_result = application_form.to_python(dict(request.POST))
             except formencode.Invalid as errors:
                 return htmlfill.render(
-                    render('admin/settings/settings.html'),
+                    base.render('admin/settings/settings.html'),
                     defaults=errors.value,
                     errors=errors.error_dict or {},
                     prefix_error=False,
@@ -219,7 +219,7 @@
         defaults.update(self._get_hg_ui_settings())
 
         return htmlfill.render(
-            render('admin/settings/settings.html'),
+            base.render('admin/settings/settings.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -233,7 +233,7 @@
                 form_result = application_form.to_python(dict(request.POST))
             except formencode.Invalid as errors:
                 return htmlfill.render(
-                    render('admin/settings/settings.html'),
+                    base.render('admin/settings/settings.html'),
                     defaults=errors.value,
                     errors=errors.error_dict or {},
                     prefix_error=False,
@@ -274,7 +274,7 @@
         defaults.update(self._get_hg_ui_settings())
 
         return htmlfill.render(
-            render('admin/settings/settings.html'),
+            base.render('admin/settings/settings.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -312,7 +312,7 @@
         c.ini = kallithea.CONFIG
 
         return htmlfill.render(
-            render('admin/settings/settings.html'),
+            base.render('admin/settings/settings.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -367,7 +367,7 @@
         c.custom_hooks = db.Ui.get_custom_hooks()
 
         return htmlfill.render(
-            render('admin/settings/settings.html'),
+            base.render('admin/settings/settings.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -386,7 +386,7 @@
         defaults.update(self._get_hg_ui_settings())
 
         return htmlfill.render(
-            render('admin/settings/settings.html'),
+            base.render('admin/settings/settings.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -404,7 +404,7 @@
             setattr(c, key, val)
 
         return htmlfill.render(
-            render('admin/settings/settings.html'),
+            base.render('admin/settings/settings.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
--- a/kallithea/controllers/admin/user_groups.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/admin/user_groups.py	Fri Nov 13 01:06:16 2020 +0100
@@ -38,9 +38,9 @@
 from webob.exc import HTTPFound, HTTPInternalServerError
 
 import kallithea.lib.helpers as h
+from kallithea.controllers import base
 from kallithea.lib import webutils
 from kallithea.lib.auth import HasPermissionAnyDecorator, HasUserGroupPermissionLevelDecorator, LoginRequired
-from kallithea.lib.base import BaseController, render
 from kallithea.lib.exceptions import RepoGroupAssignmentError, UserGroupsAssignedException
 from kallithea.lib.utils2 import safe_int, safe_str
 from kallithea.lib.webutils import url
@@ -53,7 +53,7 @@
 log = logging.getLogger(__name__)
 
 
-class UserGroupsController(BaseController):
+class UserGroupsController(base.BaseController):
 
     @LoginRequired(allow_default_user=True)
     def _before(self, *args, **kwargs):
@@ -113,7 +113,7 @@
             "records": user_groups_data
         }
 
-        return render('admin/user_groups/user_groups.html')
+        return base.render('admin/user_groups/user_groups.html')
 
     @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
     def create(self):
@@ -134,7 +134,7 @@
             meta.Session().commit()
         except formencode.Invalid as errors:
             return htmlfill.render(
-                render('admin/user_groups/user_group_add.html'),
+                base.render('admin/user_groups/user_group_add.html'),
                 defaults=errors.value,
                 errors=errors.error_dict or {},
                 prefix_error=False,
@@ -149,7 +149,7 @@
 
     @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
     def new(self, format='html'):
-        return render('admin/user_groups/user_group_add.html')
+        return base.render('admin/user_groups/user_group_add.html')
 
     @HasUserGroupPermissionLevelDecorator('admin')
     def update(self, id):
@@ -184,7 +184,7 @@
             })
 
             return htmlfill.render(
-                render('admin/user_groups/user_group_edit.html'),
+                base.render('admin/user_groups/user_group_edit.html'),
                 defaults=defaults,
                 errors=e,
                 prefix_error=False,
@@ -221,7 +221,7 @@
         defaults = self.__load_defaults(id)
 
         return htmlfill.render(
-            render('admin/user_groups/user_group_edit.html'),
+            base.render('admin/user_groups/user_group_edit.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False
@@ -243,7 +243,7 @@
                              p.permission.permission_name})
 
         return htmlfill.render(
-            render('admin/user_groups/user_group_edit.html'),
+            base.render('admin/user_groups/user_group_edit.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False
@@ -344,7 +344,7 @@
         })
 
         return htmlfill.render(
-            render('admin/user_groups/user_group_edit.html'),
+            base.render('admin/user_groups/user_group_edit.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False
@@ -394,7 +394,7 @@
         c.active = 'advanced'
         c.group_members_obj = sorted((x.user for x in c.user_group.members),
                                      key=lambda u: u.username.lower())
-        return render('admin/user_groups/user_group_edit.html')
+        return base.render('admin/user_groups/user_group_edit.html')
 
     @HasUserGroupPermissionLevelDecorator('admin')
     def edit_members(self, id):
@@ -404,4 +404,4 @@
                                      key=lambda u: u.username.lower())
 
         c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
-        return render('admin/user_groups/user_group_edit.html')
+        return base.render('admin/user_groups/user_group_edit.html')
--- a/kallithea/controllers/admin/users.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/admin/users.py	Fri Nov 13 01:06:16 2020 +0100
@@ -38,9 +38,9 @@
 
 import kallithea
 import kallithea.lib.helpers as h
+from kallithea.controllers import base
 from kallithea.lib import auth_modules, webutils
 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator, LoginRequired
-from kallithea.lib.base import BaseController, IfSshEnabled, render
 from kallithea.lib.exceptions import DefaultUserException, UserCreationError, UserOwnsReposException
 from kallithea.lib.utils2 import datetime_to_time, fmt_date, generate_api_key, safe_int
 from kallithea.lib.webutils import url
@@ -54,7 +54,7 @@
 log = logging.getLogger(__name__)
 
 
-class UsersController(BaseController):
+class UsersController(base.BaseController):
 
     @LoginRequired()
     @HasPermissionAnyDecorator('hg.admin')
@@ -103,7 +103,7 @@
             "records": users_data
         }
 
-        return render('admin/users/users.html')
+        return base.render('admin/users/users.html')
 
     def create(self):
         c.default_extern_type = db.User.DEFAULT_AUTH_TYPE
@@ -120,7 +120,7 @@
             meta.Session().commit()
         except formencode.Invalid as errors:
             return htmlfill.render(
-                render('admin/users/user_add.html'),
+                base.render('admin/users/user_add.html'),
                 defaults=errors.value,
                 errors=errors.error_dict or {},
                 prefix_error=False,
@@ -137,7 +137,7 @@
     def new(self, format='html'):
         c.default_extern_type = db.User.DEFAULT_AUTH_TYPE
         c.default_extern_name = ''
-        return render('admin/users/user_add.html')
+        return base.render('admin/users/user_add.html')
 
     def update(self, id):
         user_model = UserModel()
@@ -208,7 +208,7 @@
         c.perm_user = AuthUser(dbuser=user)
         managed_fields = auth_modules.get_managed_fields(user)
         c.readonly = lambda n: 'readonly' if n in managed_fields else None
-        return render('admin/users/user_edit.html')
+        return base.render('admin/users/user_edit.html')
 
     def edit(self, id, format='html'):
         user = self._get_user_or_raise_if_default(id)
@@ -234,7 +234,7 @@
             'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
         })
         return htmlfill.render(
-            render('admin/users/user_edit.html'),
+            base.render('admin/users/user_edit.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -255,7 +255,7 @@
                                                      show_expired=show_expired)
         defaults = c.user.get_dict()
         return htmlfill.render(
-            render('admin/users/user_edit.html'),
+            base.render('admin/users/user_edit.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -302,7 +302,7 @@
             'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
         })
         return htmlfill.render(
-            render('admin/users/user_edit.html'),
+            base.render('admin/users/user_edit.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -350,7 +350,7 @@
 
         defaults = c.user.get_dict()
         return htmlfill.render(
-            render('admin/users/user_edit.html'),
+            base.render('admin/users/user_edit.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -393,7 +393,7 @@
 
         defaults = c.user.get_dict()
         return htmlfill.render(
-            render('admin/users/user_edit.html'),
+            base.render('admin/users/user_edit.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -429,19 +429,19 @@
             raise HTTPFound(location=url('admin_permissions_ips'))
         raise HTTPFound(location=url('edit_user_ips', id=id))
 
-    @IfSshEnabled
+    @base.IfSshEnabled
     def edit_ssh_keys(self, id):
         c.user = self._get_user_or_raise_if_default(id)
         c.active = 'ssh_keys'
         c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
         defaults = c.user.get_dict()
         return htmlfill.render(
-            render('admin/users/user_edit.html'),
+            base.render('admin/users/user_edit.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
 
-    @IfSshEnabled
+    @base.IfSshEnabled
     def ssh_keys_add(self, id):
         c.user = self._get_user_or_raise_if_default(id)
 
@@ -457,7 +457,7 @@
             webutils.flash(e.args[0], category='error')
         raise HTTPFound(location=url('edit_user_ssh_keys', id=c.user.user_id))
 
-    @IfSshEnabled
+    @base.IfSshEnabled
     def ssh_keys_delete(self, id):
         c.user = self._get_user_or_raise_if_default(id)
 
--- a/kallithea/controllers/api/__init__.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/api/__init__.py	Fri Nov 13 01:06:16 2020 +0100
@@ -35,9 +35,9 @@
 from tg import Response, TGController, request, response
 from webob.exc import HTTPError, HTTPException
 
+from kallithea.controllers import base
 from kallithea.lib import ext_json
 from kallithea.lib.auth import AuthUser
-from kallithea.lib.base import get_ip_addr, get_path_info
 from kallithea.lib.utils2 import ascii_bytes
 from kallithea.model import db
 
@@ -99,7 +99,7 @@
 
         environ = state.request.environ
         start = time.time()
-        ip_addr = get_ip_addr(environ)
+        ip_addr = base.get_ip_addr(environ)
         self._req_id = None
         if 'CONTENT_LENGTH' not in environ:
             log.debug("No Content-Length")
@@ -204,8 +204,8 @@
         self._rpc_args['environ'] = environ
 
         log.info('IP: %s Request to %s time: %.3fs' % (
-            get_ip_addr(environ),
-            get_path_info(environ), time.time() - start)
+            base.get_ip_addr(environ),
+            base.get_path_info(environ), time.time() - start)
         )
 
         state.set_action(self._rpc_call, [])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kallithea/controllers/base.py	Fri Nov 13 01:06:16 2020 +0100
@@ -0,0 +1,639 @@
+# -*- coding: utf-8 -*-
+# This program 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.
+#
+# 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 General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+kallithea.controllers.base
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The base Controller API
+Provides the BaseController class for subclassing. And usage in different
+controllers
+
+This file was forked by the Kallithea project in July 2014.
+Original author and date, and relevant copyright and licensing information is below:
+:created_on: Oct 06, 2010
+:author: marcink
+:copyright: (c) 2013 RhodeCode GmbH, and others.
+:license: GPLv3, see LICENSE.md for more details.
+"""
+
+import base64
+import datetime
+import logging
+import traceback
+import warnings
+
+import decorator
+import paste.auth.basic
+import paste.httpexceptions
+import paste.httpheaders
+import webob.exc
+from tg import TGController, config, render_template, request, response, session
+from tg import tmpl_context as c
+from tg.i18n import ugettext as _
+
+import kallithea
+from kallithea.lib import auth_modules, ext_json, webutils
+from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware
+from kallithea.lib.exceptions import UserCreationError
+from kallithea.lib.utils import get_repo_slug, is_valid_repo
+from kallithea.lib.utils2 import AttributeDict, asbool, ascii_bytes, safe_int, safe_str, set_hook_environment
+from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError
+from kallithea.lib.webutils import url
+from kallithea.model import db, meta
+from kallithea.model.scm import ScmModel
+
+
+log = logging.getLogger(__name__)
+
+
+def render(template_path):
+    return render_template({'url': url}, 'mako', template_path)
+
+
+def _filter_proxy(ip):
+    """
+    HEADERS can have multiple ips inside the left-most being the original
+    client, and each successive proxy that passed the request adding the IP
+    address where it received the request from.
+
+    :param ip:
+    """
+    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 get_ip_addr(environ):
+    proxy_key = 'HTTP_X_REAL_IP'
+    proxy_key2 = 'HTTP_X_FORWARDED_FOR'
+    def_key = 'REMOTE_ADDR'
+
+    ip = environ.get(proxy_key)
+    if ip:
+        return _filter_proxy(ip)
+
+    ip = environ.get(proxy_key2)
+    if ip:
+        return _filter_proxy(ip)
+
+    ip = environ.get(def_key, '0.0.0.0')
+    return _filter_proxy(ip)
+
+
+def get_path_info(environ):
+    """Return PATH_INFO from environ ... using tg.original_request if available.
+
+    In Python 3 WSGI, PATH_INFO is a unicode str, but kind of contains encoded
+    bytes. The code points are guaranteed to only use the lower 8 bit bits, and
+    encoding the string with the 1:1 encoding latin1 will give the
+    corresponding byte string ... which then can be decoded to proper unicode.
+    """
+    org_req = environ.get('tg.original_request')
+    if org_req is not None:
+        environ = org_req.environ
+    return safe_str(environ['PATH_INFO'].encode('latin1'))
+
+
+def log_in_user(user, remember, is_external_auth, ip_addr):
+    """
+    Log a `User` in and update session and cookies. If `remember` is True,
+    the session cookie is set to expire in a year; otherwise, it expires at
+    the end of the browser session.
+
+    Returns populated `AuthUser` object.
+    """
+    # It should not be possible to explicitly log in as the default user.
+    assert not user.is_default_user, user
+
+    auth_user = AuthUser.make(dbuser=user, is_external_auth=is_external_auth, ip_addr=ip_addr)
+    if auth_user is None:
+        return None
+
+    user.update_lastlogin()
+    meta.Session().commit()
+
+    # Start new session to prevent session fixation attacks.
+    session.invalidate()
+    session['authuser'] = cookie = auth_user.to_cookie()
+
+    # If they want to be remembered, update the cookie.
+    # NOTE: Assumes that beaker defaults to browser session cookie.
+    if remember:
+        t = datetime.datetime.now() + datetime.timedelta(days=365)
+        session._set_cookie_expires(t)
+
+    session.save()
+
+    log.info('user %s is now authenticated and stored in '
+             'session, session attrs %s', user.username, cookie)
+
+    # dumps session attrs back to cookie
+    session._update_cookie_out()
+
+    return auth_user
+
+
+class BasicAuth(paste.auth.basic.AuthBasicAuthenticator):
+
+    def __init__(self, realm, authfunc, auth_http_code=None):
+        self.realm = realm
+        self.authfunc = authfunc
+        self._rc_auth_http_code = auth_http_code
+
+    def build_authentication(self, environ):
+        head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
+        # Consume the whole body before sending a response
+        try:
+            request_body_size = int(environ.get('CONTENT_LENGTH', 0))
+        except (ValueError):
+            request_body_size = 0
+        environ['wsgi.input'].read(request_body_size)
+        if self._rc_auth_http_code and self._rc_auth_http_code == '403':
+            # return 403 if alternative http return code is specified in
+            # Kallithea config
+            return paste.httpexceptions.HTTPForbidden(headers=head)
+        return paste.httpexceptions.HTTPUnauthorized(headers=head)
+
+    def authenticate(self, environ):
+        authorization = paste.httpheaders.AUTHORIZATION(environ)
+        if not authorization:
+            return self.build_authentication(environ)
+        (authmeth, auth) = authorization.split(' ', 1)
+        if 'basic' != authmeth.lower():
+            return self.build_authentication(environ)
+        auth = safe_str(base64.b64decode(auth.strip()))
+        _parts = auth.split(':', 1)
+        if len(_parts) == 2:
+            username, password = _parts
+            if self.authfunc(username, password, environ) is not None:
+                return username
+        return self.build_authentication(environ)
+
+    __call__ = authenticate
+
+
+class BaseVCSController(object):
+    """Base controller for handling Mercurial/Git protocol requests
+    (coming from a VCS client, and not a browser).
+    """
+
+    scm_alias = None # 'hg' / 'git'
+
+    def __init__(self, application, config):
+        self.application = application
+        self.config = config
+        # base path of repo locations
+        self.basepath = self.config['base_path']
+        # authenticate this VCS request using the authentication modules
+        self.authenticate = BasicAuth('', auth_modules.authenticate,
+                                      config.get('auth_ret_code'))
+
+    @classmethod
+    def parse_request(cls, environ):
+        """If request is parsed as a request for this VCS, return a namespace with the parsed request.
+        If the request is unknown, return None.
+        """
+        raise NotImplementedError()
+
+    def _authorize(self, environ, action, repo_name, ip_addr):
+        """Authenticate and authorize user.
+
+        Since we're dealing with a VCS client and not a browser, we only
+        support HTTP basic authentication, either directly via raw header
+        inspection, or by using container authentication to delegate the
+        authentication to the web server.
+
+        Returns (user, None) on successful authentication and authorization.
+        Returns (None, wsgi_app) to send the wsgi_app response to the client.
+        """
+        # Use anonymous access if allowed for action on repo.
+        default_user = db.User.get_default_user()
+        default_authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
+        if default_authuser is None:
+            log.debug('No anonymous access at all') # move on to proper user auth
+        else:
+            if self._check_permission(action, default_authuser, repo_name):
+                return default_authuser, None
+            log.debug('Not authorized to access this repository as anonymous user')
+
+        username = None
+        #==============================================================
+        # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
+        # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
+        #==============================================================
+
+        # try to auth based on environ, container auth methods
+        log.debug('Running PRE-AUTH for container based authentication')
+        pre_auth = auth_modules.authenticate('', '', environ)
+        if pre_auth is not None and pre_auth.get('username'):
+            username = pre_auth['username']
+        log.debug('PRE-AUTH got %s as username', username)
+
+        # If not authenticated by the container, running basic auth
+        if not username:
+            self.authenticate.realm = self.config['realm']
+            result = self.authenticate(environ)
+            if isinstance(result, str):
+                paste.httpheaders.AUTH_TYPE.update(environ, 'basic')
+                paste.httpheaders.REMOTE_USER.update(environ, result)
+                username = result
+            else:
+                return None, result.wsgi_application
+
+        #==============================================================
+        # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
+        #==============================================================
+        try:
+            user = db.User.get_by_username_or_email(username)
+        except Exception:
+            log.error(traceback.format_exc())
+            return None, webob.exc.HTTPInternalServerError()
+
+        authuser = AuthUser.make(dbuser=user, ip_addr=ip_addr)
+        if authuser is None:
+            return None, webob.exc.HTTPForbidden()
+        if not self._check_permission(action, authuser, repo_name):
+            return None, webob.exc.HTTPForbidden()
+
+        return user, None
+
+    def _handle_request(self, environ, start_response):
+        raise NotImplementedError()
+
+    def _check_permission(self, action, authuser, repo_name):
+        """
+        :param action: 'push' or 'pull'
+        :param user: `AuthUser` instance
+        :param repo_name: repository name
+        """
+        if action == 'push':
+            if not HasPermissionAnyMiddleware('repository.write',
+                                              'repository.admin')(authuser,
+                                                                  repo_name):
+                return False
+
+        elif action == 'pull':
+            #any other action need at least read permission
+            if not HasPermissionAnyMiddleware('repository.read',
+                                              'repository.write',
+                                              'repository.admin')(authuser,
+                                                                  repo_name):
+                return False
+
+        else:
+            assert False, action
+
+        return True
+
+    def __call__(self, environ, start_response):
+        try:
+            # try parsing a request for this VCS - if it fails, call the wrapped app
+            parsed_request = self.parse_request(environ)
+            if parsed_request is None:
+                return self.application(environ, start_response)
+
+            # skip passing error to error controller
+            environ['pylons.status_code_redirect'] = True
+
+            # quick check if repo exists...
+            if not is_valid_repo(parsed_request.repo_name, self.basepath, self.scm_alias):
+                raise webob.exc.HTTPNotFound()
+
+            if parsed_request.action is None:
+                # Note: the client doesn't get the helpful error message
+                raise webob.exc.HTTPBadRequest('Unable to detect pull/push action for %r! Are you using a nonstandard command or client?' % parsed_request.repo_name)
+
+            #======================================================================
+            # CHECK PERMISSIONS
+            #======================================================================
+            ip_addr = get_ip_addr(environ)
+            user, response_app = self._authorize(environ, parsed_request.action, parsed_request.repo_name, ip_addr)
+            if response_app is not None:
+                return response_app(environ, start_response)
+
+            #======================================================================
+            # REQUEST HANDLING
+            #======================================================================
+            set_hook_environment(user.username, ip_addr,
+                parsed_request.repo_name, self.scm_alias, parsed_request.action)
+
+            try:
+                log.info('%s action on %s repo "%s" by "%s" from %s',
+                         parsed_request.action, self.scm_alias, parsed_request.repo_name, user.username, ip_addr)
+                app = self._make_app(parsed_request)
+                return app(environ, start_response)
+            except Exception:
+                log.error(traceback.format_exc())
+                raise webob.exc.HTTPInternalServerError()
+
+        except webob.exc.HTTPException as e:
+            return e(environ, start_response)
+
+
+class BaseController(TGController):
+
+    def _before(self, *args, **kwargs):
+        """
+        _before is called before controller methods and after __call__
+        """
+        if request.needs_csrf_check:
+            # CSRF protection: Whenever a request has ambient authority (whether
+            # through a session cookie or its origin IP address), it must include
+            # the correct token, unless the HTTP method is GET or HEAD (and thus
+            # guaranteed to be side effect free. In practice, the only situation
+            # where we allow side effects without ambient authority is when the
+            # authority comes from an API key; and that is handled above.
+            token = request.POST.get(webutils.session_csrf_secret_name)
+            if not token or token != webutils.session_csrf_secret_token():
+                log.error('CSRF check failed')
+                raise webob.exc.HTTPForbidden()
+
+        c.kallithea_version = kallithea.__version__
+        settings = db.Setting.get_app_settings()
+
+        # Visual options
+        c.visual = AttributeDict({})
+
+        ## DB stored
+        c.visual.show_public_icon = asbool(settings.get('show_public_icon'))
+        c.visual.show_private_icon = asbool(settings.get('show_private_icon'))
+        c.visual.stylify_metalabels = asbool(settings.get('stylify_metalabels'))
+        c.visual.page_size = safe_int(settings.get('dashboard_items', 100))
+        c.visual.admin_grid_items = safe_int(settings.get('admin_grid_items', 100))
+        c.visual.repository_fields = asbool(settings.get('repository_fields'))
+        c.visual.show_version = asbool(settings.get('show_version'))
+        c.visual.use_gravatar = asbool(settings.get('use_gravatar'))
+        c.visual.gravatar_url = settings.get('gravatar_url')
+
+        c.ga_code = settings.get('ga_code')
+        # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code
+        if c.ga_code and '<' not in c.ga_code:
+            c.ga_code = '''<script type="text/javascript">
+                var _gaq = _gaq || [];
+                _gaq.push(['_setAccount', '%s']);
+                _gaq.push(['_trackPageview']);
+
+                (function() {
+                    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+                    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+                    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+                    })();
+            </script>''' % c.ga_code
+        c.site_name = settings.get('title')
+        c.clone_uri_tmpl = settings.get('clone_uri_tmpl') or db.Repository.DEFAULT_CLONE_URI
+        c.clone_ssh_tmpl = settings.get('clone_ssh_tmpl') or db.Repository.DEFAULT_CLONE_SSH
+
+        ## INI stored
+        c.visual.allow_repo_location_change = asbool(config.get('allow_repo_location_change', True))
+        c.visual.allow_custom_hooks_settings = asbool(config.get('allow_custom_hooks_settings', True))
+        c.ssh_enabled = asbool(config.get('ssh_enabled', False))
+
+        c.instance_id = config.get('instance_id')
+        c.issues_url = config.get('bugtracker', url('issues_url'))
+        # END CONFIG VARS
+
+        c.repo_name = get_repo_slug(request)  # can be empty
+        c.backends = list(kallithea.BACKENDS)
+
+        self.cut_off_limit = safe_int(config.get('cut_off_limit'))
+
+        c.my_pr_count = db.PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count()
+
+        self.scm_model = ScmModel()
+
+    @staticmethod
+    def _determine_auth_user(session_authuser, ip_addr):
+        """
+        Create an `AuthUser` object given the API key/bearer token
+        (if any) and the value of the authuser session cookie.
+        Returns None if no valid user is found (like not active or no access for IP).
+        """
+
+        # Authenticate by session cookie
+        # In ancient login sessions, 'authuser' may not be a dict.
+        # In that case, the user will have to log in again.
+        # v0.3 and earlier included an 'is_authenticated' key; if present,
+        # this must be True.
+        if isinstance(session_authuser, dict) and session_authuser.get('is_authenticated', True):
+            return AuthUser.from_cookie(session_authuser, ip_addr=ip_addr)
+
+        # Authenticate by auth_container plugin (if enabled)
+        if any(
+            plugin.is_container_auth
+            for plugin in auth_modules.get_auth_plugins()
+        ):
+            try:
+                user_info = auth_modules.authenticate('', '', request.environ)
+            except UserCreationError as e:
+                webutils.flash(e, 'error', logf=log.error)
+            else:
+                if user_info is not None:
+                    username = user_info['username']
+                    user = db.User.get_by_username(username, case_insensitive=True)
+                    return log_in_user(user, remember=False, is_external_auth=True, ip_addr=ip_addr)
+
+        # User is default user (if active) or anonymous
+        default_user = db.User.get_default_user()
+        authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
+        if authuser is None: # fall back to anonymous
+            authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make?
+        return authuser
+
+    @staticmethod
+    def _basic_security_checks():
+        """Perform basic security/sanity checks before processing the request."""
+
+        # Only allow the following HTTP request methods.
+        if request.method not in ['GET', 'HEAD', 'POST']:
+            raise webob.exc.HTTPMethodNotAllowed()
+
+        # Also verify the _method override - no longer allowed.
+        if request.params.get('_method') is None:
+            pass # no override, no problem
+        else:
+            raise webob.exc.HTTPMethodNotAllowed()
+
+        # Make sure CSRF token never appears in the URL. If so, invalidate it.
+        if webutils.session_csrf_secret_name in request.GET:
+            log.error('CSRF key leak detected')
+            session.pop(webutils.session_csrf_secret_name, None)
+            session.save()
+            webutils.flash(_('CSRF token leak has been detected - all form tokens have been expired'),
+                    category='error')
+
+        # WebOb already ignores request payload parameters for anything other
+        # than POST/PUT, but double-check since other Kallithea code relies on
+        # this assumption.
+        if request.method not in ['POST', 'PUT'] and request.POST:
+            log.error('%r request with payload parameters; WebOb should have stopped this', request.method)
+            raise webob.exc.HTTPBadRequest()
+
+    def __call__(self, environ, context):
+        try:
+            ip_addr = get_ip_addr(environ)
+            self._basic_security_checks()
+
+            api_key = request.GET.get('api_key')
+            try:
+                # Request.authorization may raise ValueError on invalid input
+                type, params = request.authorization
+            except (ValueError, TypeError):
+                pass
+            else:
+                if type.lower() == 'bearer':
+                    api_key = params # bearer token is an api key too
+
+            if api_key is None:
+                authuser = self._determine_auth_user(
+                    session.get('authuser'),
+                    ip_addr=ip_addr,
+                )
+                needs_csrf_check = request.method not in ['GET', 'HEAD']
+
+            else:
+                dbuser = db.User.get_by_api_key(api_key)
+                if dbuser is None:
+                    log.info('No db user found for authentication with API key ****%s from %s',
+                             api_key[-4:], ip_addr)
+                authuser = AuthUser.make(dbuser=dbuser, is_external_auth=True, ip_addr=ip_addr)
+                needs_csrf_check = False # API key provides CSRF protection
+
+            if authuser is None:
+                log.info('No valid user found')
+                raise webob.exc.HTTPForbidden()
+
+            # set globals for auth user
+            request.authuser = authuser
+            request.ip_addr = ip_addr
+            request.needs_csrf_check = needs_csrf_check
+
+            log.info('IP: %s User: %s Request: %s',
+                request.ip_addr, request.authuser,
+                get_path_info(environ),
+            )
+            return super(BaseController, self).__call__(environ, context)
+        except webob.exc.HTTPException as e:
+            return e
+
+
+class BaseRepoController(BaseController):
+    """
+    Base class for controllers responsible for loading all needed data for
+    repository loaded items are
+
+    c.db_repo_scm_instance: instance of scm repository
+    c.db_repo: instance of db
+    c.repository_followers: number of followers
+    c.repository_forks: number of forks
+    c.repository_following: weather the current user is following the current repo
+    """
+
+    def _before(self, *args, **kwargs):
+        super(BaseRepoController, self)._before(*args, **kwargs)
+        if c.repo_name:  # extracted from request by base-base BaseController._before
+            _dbr = db.Repository.get_by_repo_name(c.repo_name)
+            if not _dbr:
+                return
+
+            log.debug('Found repository in database %s with state `%s`',
+                      _dbr, _dbr.repo_state)
+            route = getattr(request.environ.get('routes.route'), 'name', '')
+
+            # allow to delete repos that are somehow damages in filesystem
+            if route in ['delete_repo']:
+                return
+
+            if _dbr.repo_state in [db.Repository.STATE_PENDING]:
+                if route in ['repo_creating_home']:
+                    return
+                check_url = url('repo_creating_home', repo_name=c.repo_name)
+                raise webob.exc.HTTPFound(location=check_url)
+
+            dbr = c.db_repo = _dbr
+            c.db_repo_scm_instance = c.db_repo.scm_instance
+            if c.db_repo_scm_instance is None:
+                log.error('%s this repository is present in database but it '
+                          'cannot be created as an scm instance', c.repo_name)
+                webutils.flash(_('Repository not found in the filesystem'),
+                        category='error')
+                raise webob.exc.HTTPNotFound()
+
+            # some globals counter for menu
+            c.repository_followers = self.scm_model.get_followers(dbr)
+            c.repository_forks = self.scm_model.get_forks(dbr)
+            c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
+            c.repository_following = self.scm_model.is_following_repo(
+                                    c.repo_name, request.authuser.user_id)
+
+    @staticmethod
+    def _get_ref_rev(repo, ref_type, ref_name, returnempty=False):
+        """
+        Safe way to get changeset. If error occurs show error.
+        """
+        try:
+            return repo.scm_instance.get_ref_revision(ref_type, ref_name)
+        except EmptyRepositoryError as e:
+            if returnempty:
+                return repo.scm_instance.EMPTY_CHANGESET
+            webutils.flash(_('There are no changesets yet'), category='error')
+            raise webob.exc.HTTPNotFound()
+        except ChangesetDoesNotExistError as e:
+            webutils.flash(_('Changeset for %s %s not found in %s') %
+                              (ref_type, ref_name, repo.repo_name),
+                    category='error')
+            raise webob.exc.HTTPNotFound()
+        except RepositoryError as e:
+            log.error(traceback.format_exc())
+            webutils.flash(e, category='error')
+            raise webob.exc.HTTPBadRequest()
+
+
+@decorator.decorator
+def jsonify(func, *args, **kwargs):
+    """Action decorator that formats output for JSON
+
+    Given a function that will return content, this decorator will turn
+    the result into JSON, with a content-type of 'application/json' and
+    output it.
+    """
+    response.headers['Content-Type'] = 'application/json; charset=utf-8'
+    data = func(*args, **kwargs)
+    if isinstance(data, (list, tuple)):
+        # A JSON list response is syntactically valid JavaScript and can be
+        # loaded and executed as JavaScript by a malicious third-party site
+        # using <script>, which can lead to cross-site data leaks.
+        # JSON responses should therefore be scalars or objects (i.e. Python
+        # dicts), because a JSON object is a syntax error if intepreted as JS.
+        msg = "JSON responses with Array envelopes are susceptible to " \
+              "cross-site data leak attacks, see " \
+              "https://web.archive.org/web/20120519231904/http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
+        warnings.warn(msg, Warning, 2)
+        log.warning(msg)
+    log.debug("Returning JSON wrapped action output")
+    return ascii_bytes(ext_json.dumps(data))
+
+@decorator.decorator
+def IfSshEnabled(func, *args, **kwargs):
+    """Decorator for functions that can only be called if SSH access is enabled.
+
+    If SSH access is disabled in the configuration file, HTTPNotFound is raised.
+    """
+    if not c.ssh_enabled:
+        webutils.flash(_("SSH access is disabled."), category='warning')
+        raise webob.exc.HTTPNotFound()
+    return func(*args, **kwargs)
--- a/kallithea/controllers/changelog.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/changelog.py	Fri Nov 13 01:06:16 2020 +0100
@@ -33,9 +33,9 @@
 from tg.i18n import ugettext as _
 from webob.exc import HTTPBadRequest, HTTPFound, HTTPNotFound
 
+from kallithea.controllers import base
 from kallithea.lib import webutils
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
-from kallithea.lib.base import BaseRepoController, render
 from kallithea.lib.graphmod import graph_data
 from kallithea.lib.page import Page
 from kallithea.lib.utils2 import safe_int
@@ -46,7 +46,7 @@
 log = logging.getLogger(__name__)
 
 
-class ChangelogController(BaseRepoController):
+class ChangelogController(base.BaseRepoController):
 
     def _before(self, *args, **kwargs):
         super(ChangelogController, self)._before(*args, **kwargs)
@@ -146,12 +146,12 @@
 
         c.revision = revision # requested revision ref
         c.first_revision = c.cs_pagination[0] # pagination is never empty here!
-        return render('changelog/changelog.html')
+        return base.render('changelog/changelog.html')
 
     @LoginRequired(allow_default_user=True)
     @HasRepoPermissionLevelDecorator('read')
     def changelog_details(self, cs):
         if request.environ.get('HTTP_X_PARTIAL_XHR'):
             c.cs = c.db_repo_scm_instance.get_changeset(cs)
-            return render('changelog/changelog_details.html')
+            return base.render('changelog/changelog_details.html')
         raise HTTPNotFound()
--- a/kallithea/controllers/changeset.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/changeset.py	Fri Nov 13 01:06:16 2020 +0100
@@ -36,9 +36,9 @@
 from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPNotFound
 
 import kallithea.lib.helpers as h
+from kallithea.controllers import base
 from kallithea.lib import auth, diffs, webutils
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
-from kallithea.lib.base import BaseRepoController, jsonify, render
 from kallithea.lib.graphmod import graph_data
 from kallithea.lib.utils2 import ascii_str, safe_str
 from kallithea.lib.vcs.backends.base import EmptyChangeset
@@ -148,7 +148,7 @@
         c.comment = comment
         data.update(comment.get_dict())
         data.update({'rendered_text':
-                     render('changeset/changeset_comment_block.html')})
+                     base.render('changeset/changeset_comment_block.html')})
 
     return data
 
@@ -170,7 +170,7 @@
     else:
         raise HTTPForbidden()
 
-class ChangesetController(BaseRepoController):
+class ChangesetController(base.BaseRepoController):
 
     def _before(self, *args, **kwargs):
         super(ChangesetController, self)._before(*args, **kwargs)
@@ -285,19 +285,19 @@
         elif method == 'patch':
             response.content_type = 'text/plain'
             c.diff = safe_str(raw_diff)
-            return render('changeset/patch_changeset.html')
+            return base.render('changeset/patch_changeset.html')
         elif method == 'raw':
             response.content_type = 'text/plain'
             return raw_diff
         elif method == 'show':
             if len(c.cs_ranges) == 1:
-                return render('changeset/changeset.html')
+                return base.render('changeset/changeset.html')
             else:
                 c.cs_ranges_org = None
                 c.cs_comments = {}
                 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
                 c.jsdata = graph_data(c.db_repo_scm_instance, revs)
-                return render('changeset/changeset_range.html')
+                return base.render('changeset/changeset_range.html')
 
     @LoginRequired(allow_default_user=True)
     @HasRepoPermissionLevelDecorator('read')
@@ -321,19 +321,19 @@
 
     @LoginRequired()
     @HasRepoPermissionLevelDecorator('read')
-    @jsonify
+    @base.jsonify
     def comment(self, repo_name, revision):
         return create_cs_pr_comment(repo_name, revision=revision)
 
     @LoginRequired()
     @HasRepoPermissionLevelDecorator('read')
-    @jsonify
+    @base.jsonify
     def delete_comment(self, repo_name, comment_id):
         return delete_cs_pr_comment(repo_name, comment_id)
 
     @LoginRequired(allow_default_user=True)
     @HasRepoPermissionLevelDecorator('read')
-    @jsonify
+    @base.jsonify
     def changeset_info(self, repo_name, revision):
         if request.is_xhr:
             try:
@@ -345,7 +345,7 @@
 
     @LoginRequired(allow_default_user=True)
     @HasRepoPermissionLevelDecorator('read')
-    @jsonify
+    @base.jsonify
     def changeset_children(self, repo_name, revision):
         if request.is_xhr:
             changeset = c.db_repo_scm_instance.get_changeset(revision)
@@ -358,7 +358,7 @@
 
     @LoginRequired(allow_default_user=True)
     @HasRepoPermissionLevelDecorator('read')
-    @jsonify
+    @base.jsonify
     def changeset_parents(self, repo_name, revision):
         if request.is_xhr:
             changeset = c.db_repo_scm_instance.get_changeset(revision)
--- a/kallithea/controllers/compare.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/compare.py	Fri Nov 13 01:06:16 2020 +0100
@@ -35,9 +35,9 @@
 from webob.exc import HTTPBadRequest, HTTPFound, HTTPNotFound
 
 import kallithea.lib.helpers as h
+from kallithea.controllers import base
 from kallithea.lib import diffs, webutils
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
-from kallithea.lib.base import BaseRepoController, render
 from kallithea.lib.graphmod import graph_data
 from kallithea.lib.webutils import url
 from kallithea.model import db
@@ -46,7 +46,7 @@
 log = logging.getLogger(__name__)
 
 
-class CompareController(BaseRepoController):
+class CompareController(base.BaseRepoController):
 
     def _before(self, *args, **kwargs):
         super(CompareController, self)._before(*args, **kwargs)
@@ -76,7 +76,7 @@
     def index(self, repo_name):
         c.compare_home = True
         c.a_ref_name = c.cs_ref_name = None
-        return render('compare/compare_diff.html')
+        return base.render('compare/compare_diff.html')
 
     @LoginRequired(allow_default_user=True)
     @HasRepoPermissionLevelDecorator('read')
@@ -129,7 +129,7 @@
         c.jsdata = graph_data(c.cs_repo.scm_instance, revs)
 
         if partial:
-            return render('compare/compare_cs.html')
+            return base.render('compare/compare_cs.html')
 
         org_repo = c.a_repo
         other_repo = c.cs_repo
@@ -185,4 +185,4 @@
             html_diff = diffs.as_html(parsed_lines=[f])
             c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, html_diff, st))
 
-        return render('compare/compare_diff.html')
+        return base.render('compare/compare_diff.html')
--- a/kallithea/controllers/error.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/error.py	Fri Nov 13 01:06:16 2020 +0100
@@ -32,13 +32,13 @@
 from tg import tmpl_context as c
 from tg.i18n import ugettext as _
 
-from kallithea.lib.base import BaseController
+from kallithea.controllers import base
 
 
 log = logging.getLogger(__name__)
 
 
-class ErrorController(BaseController):
+class ErrorController(base.BaseController):
     """Generates error documents as and when they are required.
 
     The errorpage middleware renders /error/document when error
--- a/kallithea/controllers/feed.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/feed.py	Fri Nov 13 01:06:16 2020 +0100
@@ -35,9 +35,9 @@
 
 import kallithea
 import kallithea.lib.helpers as h
+from kallithea.controllers import base
 from kallithea.lib import feeds, webutils
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
-from kallithea.lib.base import BaseRepoController
 from kallithea.lib.diffs import DiffProcessor
 from kallithea.lib.utils2 import asbool, fmt_date, safe_int, safe_str, shorter
 
@@ -45,7 +45,7 @@
 log = logging.getLogger(__name__)
 
 
-class FeedController(BaseRepoController):
+class FeedController(base.BaseRepoController):
 
     @LoginRequired(allow_default_user=True)
     @HasRepoPermissionLevelDecorator('read')
--- a/kallithea/controllers/files.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/files.py	Fri Nov 13 01:06:16 2020 +0100
@@ -40,9 +40,9 @@
 
 import kallithea
 import kallithea.lib.helpers as h
+from kallithea.controllers import base
 from kallithea.lib import diffs, webutils
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
-from kallithea.lib.base import BaseRepoController, jsonify, render
 from kallithea.lib.exceptions import NonRelativePathError
 from kallithea.lib.utils2 import asbool, convert_line_endings, detect_mode, safe_str
 from kallithea.lib.vcs.backends.base import EmptyChangeset
@@ -60,7 +60,7 @@
 log = logging.getLogger(__name__)
 
 
-class FilesController(BaseRepoController):
+class FilesController(base.BaseRepoController):
 
     def _before(self, *args, **kwargs):
         super(FilesController, self)._before(*args, **kwargs)
@@ -179,7 +179,7 @@
             raise HTTPNotFound()
 
         if request.environ.get('HTTP_X_PARTIAL_XHR'):
-            return render('files/files_ypjax.html')
+            return base.render('files/files_ypjax.html')
 
         # TODO: tags and bookmarks?
         c.revision_options = [(c.changeset.raw_id,
@@ -190,11 +190,11 @@
             c.revision_options += [('-', '-')] + \
                 [(n, prefix + b) for b, n in c.db_repo_scm_instance.closed_branches.items()]
 
-        return render('files/files.html')
+        return base.render('files/files.html')
 
     @LoginRequired(allow_default_user=True)
     @HasRepoPermissionLevelDecorator('read')
-    @jsonify
+    @base.jsonify
     def history(self, repo_name, revision, f_path):
         changeset = self.__get_cs(revision)
         _file = changeset.get_node(f_path)
@@ -224,7 +224,7 @@
             c.authors = []
             for a in set([x.author for x in _hist]):
                 c.authors.append((author_email(a), h.person(a)))
-            return render('files/files_history_box.html')
+            return base.render('files/files_history_box.html')
 
     @LoginRequired(allow_default_user=True)
     @HasRepoPermissionLevelDecorator('read')
@@ -335,7 +335,7 @@
             raise HTTPFound(location=url('changeset_home',
                                 repo_name=c.repo_name, revision='tip'))
 
-        return render('files/files_delete.html')
+        return base.render('files/files_delete.html')
 
     @LoginRequired()
     @HasRepoPermissionLevelDecorator('write')
@@ -393,7 +393,7 @@
             raise HTTPFound(location=url('changeset_home',
                                 repo_name=c.repo_name, revision='tip'))
 
-        return render('files/files_edit.html')
+        return base.render('files/files_edit.html')
 
     @LoginRequired()
     @HasRepoPermissionLevelDecorator('write')
@@ -468,7 +468,7 @@
             raise HTTPFound(location=url('changeset_home',
                                 repo_name=c.repo_name, revision='tip'))
 
-        return render('files/files_add.html')
+        return base.render('files/files_add.html')
 
     @LoginRequired(allow_default_user=True)
     @HasRepoPermissionLevelDecorator('read')
@@ -645,7 +645,7 @@
                                          ignore_whitespace=ignore_whitespace_diff,
                                          line_context=diff_context_size)
             c.file_diff_data = [(fid, fid, op, a_path, node2.path, diff, st)]
-            return render('files/file_diff.html')
+            return base.render('files/file_diff.html')
 
     @LoginRequired(allow_default_user=True)
     @HasRepoPermissionLevelDecorator('read')
@@ -693,7 +693,7 @@
         c.cs1 = c.changeset_1
         c.cs2 = c.changeset_2
 
-        return render('files/diff_2way.html')
+        return base.render('files/diff_2way.html')
 
     def _get_node_history(self, cs, f_path, changesets=None):
         """
@@ -736,7 +736,7 @@
 
     @LoginRequired(allow_default_user=True)
     @HasRepoPermissionLevelDecorator('read')
-    @jsonify
+    @base.jsonify
     def nodelist(self, repo_name, revision, f_path):
         if request.environ.get('HTTP_X_PARTIAL_XHR'):
             cs = self.__get_cs(revision)
--- a/kallithea/controllers/followers.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/followers.py	Fri Nov 13 01:06:16 2020 +0100
@@ -30,8 +30,8 @@
 from tg import request
 from tg import tmpl_context as c
 
+from kallithea.controllers import base
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
-from kallithea.lib.base import BaseRepoController, render
 from kallithea.lib.page import Page
 from kallithea.lib.utils2 import safe_int
 from kallithea.model import db
@@ -40,7 +40,7 @@
 log = logging.getLogger(__name__)
 
 
-class FollowersController(BaseRepoController):
+class FollowersController(base.BaseRepoController):
 
     @LoginRequired(allow_default_user=True)
     @HasRepoPermissionLevelDecorator('read')
@@ -52,6 +52,6 @@
         c.followers_pager = Page(d, page=p, items_per_page=20)
 
         if request.environ.get('HTTP_X_PARTIAL_XHR'):
-            return render('/followers/followers_data.html')
+            return base.render('/followers/followers_data.html')
 
-        return render('/followers/followers.html')
+        return base.render('/followers/followers.html')
--- a/kallithea/controllers/forks.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/forks.py	Fri Nov 13 01:06:16 2020 +0100
@@ -36,9 +36,9 @@
 from webob.exc import HTTPFound, HTTPNotFound
 
 import kallithea
+from kallithea.controllers import base
 from kallithea.lib import webutils
 from kallithea.lib.auth import HasPermissionAnyDecorator, HasRepoPermissionLevel, HasRepoPermissionLevelDecorator, LoginRequired
-from kallithea.lib.base import BaseRepoController, render
 from kallithea.lib.page import Page
 from kallithea.lib.utils2 import safe_int
 from kallithea.model import db
@@ -50,7 +50,7 @@
 log = logging.getLogger(__name__)
 
 
-class ForksController(BaseRepoController):
+class ForksController(base.BaseRepoController):
 
     def __load_defaults(self):
         c.repo_groups = AvailableRepoGroupChoices('write')
@@ -113,9 +113,9 @@
         c.forks_pager = Page(d, page=p, items_per_page=20)
 
         if request.environ.get('HTTP_X_PARTIAL_XHR'):
-            return render('/forks/forks_data.html')
+            return base.render('/forks/forks_data.html')
 
-        return render('/forks/forks.html')
+        return base.render('/forks/forks.html')
 
     @LoginRequired()
     @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
@@ -128,7 +128,7 @@
         defaults = self.__load_data()
 
         return htmlfill.render(
-            render('forks/fork.html'),
+            base.render('forks/fork.html'),
             defaults=defaults,
             encoding="UTF-8",
             force_defaults=False)
@@ -157,7 +157,7 @@
             task_id = task.task_id
         except formencode.Invalid as errors:
             return htmlfill.render(
-                render('forks/fork.html'),
+                base.render('forks/fork.html'),
                 defaults=errors.value,
                 errors=errors.error_dict or {},
                 prefix_error=False,
--- a/kallithea/controllers/home.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/home.py	Fri Nov 13 01:06:16 2020 +0100
@@ -35,8 +35,8 @@
 from webob.exc import HTTPBadRequest
 
 import kallithea.lib.helpers as h
+from kallithea.controllers import base
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
-from kallithea.lib.base import BaseController, jsonify, render
 from kallithea.lib.utils2 import safe_str
 from kallithea.model import db
 from kallithea.model.repo import RepoModel
@@ -46,10 +46,10 @@
 log = logging.getLogger(__name__)
 
 
-class HomeController(BaseController):
+class HomeController(base.BaseController):
 
     def about(self):
-        return render('/about.html')
+        return base.render('/about.html')
 
     @LoginRequired(allow_default_user=True)
     def index(self):
@@ -62,10 +62,10 @@
                                                repo_groups_list=repo_groups_list,
                                                short_name=True)
 
-        return render('/index.html')
+        return base.render('/index.html')
 
     @LoginRequired(allow_default_user=True)
-    @jsonify
+    @base.jsonify
     def repo_switcher_data(self):
         if request.is_xhr:
             all_repos = db.Repository.query(sorted=True).all()
@@ -109,7 +109,7 @@
 
     @LoginRequired(allow_default_user=True)
     @HasRepoPermissionLevelDecorator('read')
-    @jsonify
+    @base.jsonify
     def repo_refs_data(self, repo_name):
         repo = db.Repository.get_by_repo_name(repo_name).scm_instance
         res = []
@@ -144,7 +144,7 @@
         return data
 
     @LoginRequired()
-    @jsonify
+    @base.jsonify
     def users_and_groups_data(self):
         """
         Returns 'results' with a list of users and user groups.
--- a/kallithea/controllers/journal.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/journal.py	Fri Nov 13 01:06:16 2020 +0100
@@ -37,10 +37,10 @@
 from webob.exc import HTTPBadRequest
 
 import kallithea.lib.helpers as h
+from kallithea.controllers import base
 from kallithea.controllers.admin.admin import _journal_filter
 from kallithea.lib import feeds, webutils
 from kallithea.lib.auth import LoginRequired
-from kallithea.lib.base import BaseController, render
 from kallithea.lib.page import Page
 from kallithea.lib.utils2 import AttributeDict, safe_int
 from kallithea.model import db, meta
@@ -55,7 +55,7 @@
 feed_nr = 20
 
 
-class JournalController(BaseController):
+class JournalController(base.BaseController):
 
     def _before(self, *args, **kwargs):
         super(JournalController, self)._before(*args, **kwargs)
@@ -178,7 +178,7 @@
         c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager)
 
         if request.environ.get('HTTP_X_PARTIAL_XHR'):
-            return render('journal/journal_data.html')
+            return base.render('journal/journal_data.html')
 
         repos_list = db.Repository.query(sorted=True) \
             .filter_by(owner_id=request.authuser.user_id).all()
@@ -187,7 +187,7 @@
         # data used to render the grid
         c.data = repos_data
 
-        return render('journal/journal.html')
+        return base.render('journal/journal.html')
 
     @LoginRequired()
     def journal_atom(self):
@@ -250,9 +250,9 @@
         c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager)
 
         if request.environ.get('HTTP_X_PARTIAL_XHR'):
-            return render('journal/journal_data.html')
+            return base.render('journal/journal_data.html')
 
-        return render('journal/public_journal.html')
+        return base.render('journal/public_journal.html')
 
     @LoginRequired(allow_default_user=True)
     def public_journal_atom(self):
--- a/kallithea/controllers/login.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/login.py	Fri Nov 13 01:06:16 2020 +0100
@@ -36,9 +36,9 @@
 from tg.i18n import ugettext as _
 from webob.exc import HTTPBadRequest, HTTPFound
 
+from kallithea.controllers import base
 from kallithea.lib import webutils
 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator
-from kallithea.lib.base import BaseController, log_in_user, render
 from kallithea.lib.exceptions import UserCreationError
 from kallithea.lib.recaptcha import submit
 from kallithea.lib.webutils import url
@@ -50,7 +50,7 @@
 log = logging.getLogger(__name__)
 
 
-class LoginController(BaseController):
+class LoginController(base.BaseController):
 
     def _validate_came_from(self, came_from,
             _re=re.compile(r"/(?!/)[-!#$%&'()*+,./:;=?@_~0-9A-Za-z]*$")):
@@ -89,7 +89,7 @@
                 # remove password from filling in form again
                 defaults.pop('password', None)
                 return htmlfill.render(
-                    render('/login.html'),
+                    base.render('/login.html'),
                     defaults=errors.value,
                     errors=errors.error_dict or {},
                     prefix_error=False,
@@ -103,7 +103,7 @@
                 webutils.flash(e, 'error')
             else:
                 # login_form already validated the password - now set the session cookie accordingly
-                auth_user = log_in_user(user, c.form_result['remember'], is_external_auth=False, ip_addr=request.ip_addr)
+                auth_user = base.log_in_user(user, c.form_result['remember'], is_external_auth=False, ip_addr=request.ip_addr)
                 if auth_user:
                     raise HTTPFound(location=c.came_from)
                 webutils.flash(_('Authentication failed.'), 'error')
@@ -113,7 +113,7 @@
                 raise HTTPFound(location=c.came_from)
             # continue to show login to default user
 
-        return render('/login.html')
+        return base.render('/login.html')
 
     @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
                                'hg.register.manual_activate')
@@ -151,7 +151,7 @@
 
             except formencode.Invalid as errors:
                 return htmlfill.render(
-                    render('/register.html'),
+                    base.render('/register.html'),
                     defaults=errors.value,
                     errors=errors.error_dict or {},
                     prefix_error=False,
@@ -164,7 +164,7 @@
                 # Exception itself
                 webutils.flash(e, 'error')
 
-        return render('/register.html')
+        return base.render('/register.html')
 
     def password_reset(self):
         settings = db.Setting.get_app_settings()
@@ -193,14 +193,14 @@
 
             except formencode.Invalid as errors:
                 return htmlfill.render(
-                    render('/password_reset.html'),
+                    base.render('/password_reset.html'),
                     defaults=errors.value,
                     errors=errors.error_dict or {},
                     prefix_error=False,
                     encoding="UTF-8",
                     force_defaults=False)
 
-        return render('/password_reset.html')
+        return base.render('/password_reset.html')
 
     def password_reset_confirmation(self):
         # This controller handles both GET and POST requests, though we
@@ -213,14 +213,14 @@
         c.timestamp = request.params.get('timestamp') or ''
         c.token = request.params.get('token') or ''
         if not request.POST:
-            return render('/password_reset_confirmation.html')
+            return base.render('/password_reset_confirmation.html')
 
         form = PasswordResetConfirmationForm()()
         try:
             form_result = form.to_python(dict(request.POST))
         except formencode.Invalid as errors:
             return htmlfill.render(
-                render('/password_reset_confirmation.html'),
+                base.render('/password_reset_confirmation.html'),
                 defaults=errors.value,
                 errors=errors.error_dict or {},
                 prefix_error=False,
@@ -232,7 +232,7 @@
             form_result['token'],
         ):
             return htmlfill.render(
-                render('/password_reset_confirmation.html'),
+                base.render('/password_reset_confirmation.html'),
                 defaults=form_result,
                 errors={'token': _('Invalid password reset token')},
                 prefix_error=False,
--- a/kallithea/controllers/pullrequests.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/pullrequests.py	Fri Nov 13 01:06:16 2020 +0100
@@ -36,10 +36,10 @@
 from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPFound, HTTPNotFound
 
 import kallithea.lib.helpers as h
+from kallithea.controllers import base
 from kallithea.controllers.changeset import create_cs_pr_comment, delete_cs_pr_comment
 from kallithea.lib import auth, diffs, webutils
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
-from kallithea.lib.base import BaseRepoController, jsonify, render
 from kallithea.lib.graphmod import graph_data
 from kallithea.lib.page import Page
 from kallithea.lib.utils2 import ascii_bytes, safe_bytes, safe_int
@@ -69,7 +69,7 @@
     return user
 
 
-class PullrequestsController(BaseRepoController):
+class PullrequestsController(base.BaseRepoController):
 
     def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
         """return a structure with scm repo's interesting changesets, suitable for
@@ -211,7 +211,7 @@
 
         c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100, **url_params)
 
-        return render('/pullrequests/pullrequest_show_all.html')
+        return base.render('/pullrequests/pullrequest_show_all.html')
 
     @LoginRequired()
     def show_my(self):
@@ -236,7 +236,7 @@
             else:
                 c.participate_in_pull_requests_todo.append(pr)
 
-        return render('/pullrequests/pullrequest_show_my.html')
+        return base.render('/pullrequests/pullrequest_show_my.html')
 
     @LoginRequired()
     @HasRepoPermissionLevelDecorator('read')
@@ -291,11 +291,11 @@
         for fork in org_repo.forks:
             c.a_repos.append((fork.repo_name, fork.repo_name))
 
-        return render('/pullrequests/pullrequest.html')
+        return base.render('/pullrequests/pullrequest.html')
 
     @LoginRequired()
     @HasRepoPermissionLevelDecorator('read')
-    @jsonify
+    @base.jsonify
     def repo_info(self, repo_name):
         repo = c.db_repo
         refs, selected_ref = self._get_repo_refs(repo.scm_instance)
@@ -432,7 +432,7 @@
 
     @LoginRequired()
     @HasRepoPermissionLevelDecorator('read')
-    @jsonify
+    @base.jsonify
     def delete(self, repo_name, pull_request_id):
         pull_request = db.PullRequest.get_or_404(pull_request_id)
         # only owner can delete it !
@@ -620,11 +620,11 @@
 
         c.is_ajax_preview = False
         c.ancestors = None # [c.a_rev] ... but that is shown in an other way
-        return render('/pullrequests/pullrequest_show.html')
+        return base.render('/pullrequests/pullrequest_show.html')
 
     @LoginRequired()
     @HasRepoPermissionLevelDecorator('read')
-    @jsonify
+    @base.jsonify
     def comment(self, repo_name, pull_request_id):
         pull_request = db.PullRequest.get_or_404(pull_request_id)
         allowed_to_change_status = self._is_allowed_to_change_status(pull_request)
@@ -633,6 +633,6 @@
 
     @LoginRequired()
     @HasRepoPermissionLevelDecorator('read')
-    @jsonify
+    @base.jsonify
     def delete_comment(self, repo_name, comment_id):
         return delete_cs_pr_comment(repo_name, comment_id)
--- a/kallithea/controllers/root.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/root.py	Fri Nov 13 01:06:16 2020 +0100
@@ -14,9 +14,9 @@
 from tg import config
 from tgext.routes import RoutedController
 
+from kallithea.controllers import base
 from kallithea.controllers.error import ErrorController
 from kallithea.controllers.routing import make_map
-from kallithea.lib.base import BaseController
 
 
 # This is the main Kallithea entry point; TurboGears will forward all requests
@@ -26,7 +26,7 @@
 # The mapper is configured using routes defined in routing.py.  This use of the
 # 'mapper' attribute is a feature of tgext.routes, which is activated by
 # inheriting from its RoutedController class.
-class RootController(RoutedController, BaseController):
+class RootController(RoutedController, base.BaseController):
 
     def __init__(self):
         self.mapper = make_map(config)
--- a/kallithea/controllers/search.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/search.py	Fri Nov 13 01:06:16 2020 +0100
@@ -35,8 +35,8 @@
 from whoosh.qparser import QueryParser, QueryParserError
 from whoosh.query import Phrase, Prefix
 
+from kallithea.controllers import base
 from kallithea.lib.auth import LoginRequired
-from kallithea.lib.base import BaseRepoController, render
 from kallithea.lib.indexers import CHGSET_IDX_NAME, CHGSETS_SCHEMA, IDX_NAME, SCHEMA, WhooshResultWrapper
 from kallithea.lib.page import Page
 from kallithea.lib.utils2 import safe_int
@@ -46,7 +46,7 @@
 log = logging.getLogger(__name__)
 
 
-class SearchController(BaseRepoController):
+class SearchController(base.BaseRepoController):
 
     @LoginRequired(allow_default_user=True)
     def index(self, repo_name=None):
@@ -139,4 +139,4 @@
                 c.runtime = _('An error occurred during search operation.')
 
         # Return a rendered template
-        return render('/search/search.html')
+        return base.render('/search/search.html')
--- a/kallithea/controllers/summary.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/kallithea/controllers/summary.py	Fri Nov 13 01:06:16 2020 +0100
@@ -38,9 +38,9 @@
 from tg.i18n import ugettext as _
 from webob.exc import HTTPBadRequest
 
+from kallithea.controllers import base
 from kallithea.lib import ext_json, webutils
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
-from kallithea.lib.base import BaseRepoController, jsonify, render
 from kallithea.lib.conf import ALL_EXTS, ALL_READMES, LANGUAGES_EXTENSIONS_MAP
 from kallithea.lib.markup_renderer import MarkupRenderer
 from kallithea.lib.page import Page
@@ -58,7 +58,7 @@
                            key=lambda y:y[0][1] + y[1][1])]
 
 
-class SummaryController(BaseRepoController):
+class SummaryController(base.BaseRepoController):
 
     def __get_readme_data(self, db_repo):
         repo_name = db_repo.repo_name
@@ -148,11 +148,11 @@
         c.enable_downloads = c.db_repo.enable_downloads
         c.readme_data, c.readme_file = \
             self.__get_readme_data(c.db_repo)
-        return render('summary/summary.html')
+        return base.render('summary/summary.html')
 
     @LoginRequired()
     @HasRepoPermissionLevelDecorator('read')
-    @jsonify
+    @base.jsonify
     def repo_size(self, repo_name):
         if request.is_xhr:
             return c.db_repo._repo_size()
@@ -209,4 +209,4 @@
 
         recurse_limit = 500  # don't recurse more than 500 times when parsing
         async_tasks.get_commits_stats(c.db_repo.repo_name, ts_min_y, ts_max_y, recurse_limit)
-        return render('summary/statistics.html')
+        return base.render('summary/statistics.html')
--- a/kallithea/lib/base.py	Mon Dec 14 23:00:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,639 +0,0 @@
-# -*- coding: utf-8 -*-
-# This program 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.
-#
-# 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 General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-"""
-kallithea.lib.base
-~~~~~~~~~~~~~~~~~~
-
-The base Controller API
-Provides the BaseController class for subclassing. And usage in different
-controllers
-
-This file was forked by the Kallithea project in July 2014.
-Original author and date, and relevant copyright and licensing information is below:
-:created_on: Oct 06, 2010
-:author: marcink
-:copyright: (c) 2013 RhodeCode GmbH, and others.
-:license: GPLv3, see LICENSE.md for more details.
-"""
-
-import base64
-import datetime
-import logging
-import traceback
-import warnings
-
-import decorator
-import paste.auth.basic
-import paste.httpexceptions
-import paste.httpheaders
-import webob.exc
-from tg import TGController, config, render_template, request, response, session
-from tg import tmpl_context as c
-from tg.i18n import ugettext as _
-
-import kallithea
-from kallithea.lib import auth_modules, ext_json, webutils
-from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware
-from kallithea.lib.exceptions import UserCreationError
-from kallithea.lib.utils import get_repo_slug, is_valid_repo
-from kallithea.lib.utils2 import AttributeDict, asbool, ascii_bytes, safe_int, safe_str, set_hook_environment
-from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError
-from kallithea.lib.webutils import url
-from kallithea.model import db, meta
-from kallithea.model.scm import ScmModel
-
-
-log = logging.getLogger(__name__)
-
-
-def render(template_path):
-    return render_template({'url': url}, 'mako', template_path)
-
-
-def _filter_proxy(ip):
-    """
-    HEADERS can have multiple ips inside the left-most being the original
-    client, and each successive proxy that passed the request adding the IP
-    address where it received the request from.
-
-    :param ip:
-    """
-    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 get_ip_addr(environ):
-    proxy_key = 'HTTP_X_REAL_IP'
-    proxy_key2 = 'HTTP_X_FORWARDED_FOR'
-    def_key = 'REMOTE_ADDR'
-
-    ip = environ.get(proxy_key)
-    if ip:
-        return _filter_proxy(ip)
-
-    ip = environ.get(proxy_key2)
-    if ip:
-        return _filter_proxy(ip)
-
-    ip = environ.get(def_key, '0.0.0.0')
-    return _filter_proxy(ip)
-
-
-def get_path_info(environ):
-    """Return PATH_INFO from environ ... using tg.original_request if available.
-
-    In Python 3 WSGI, PATH_INFO is a unicode str, but kind of contains encoded
-    bytes. The code points are guaranteed to only use the lower 8 bit bits, and
-    encoding the string with the 1:1 encoding latin1 will give the
-    corresponding byte string ... which then can be decoded to proper unicode.
-    """
-    org_req = environ.get('tg.original_request')
-    if org_req is not None:
-        environ = org_req.environ
-    return safe_str(environ['PATH_INFO'].encode('latin1'))
-
-
-def log_in_user(user, remember, is_external_auth, ip_addr):
-    """
-    Log a `User` in and update session and cookies. If `remember` is True,
-    the session cookie is set to expire in a year; otherwise, it expires at
-    the end of the browser session.
-
-    Returns populated `AuthUser` object.
-    """
-    # It should not be possible to explicitly log in as the default user.
-    assert not user.is_default_user, user
-
-    auth_user = AuthUser.make(dbuser=user, is_external_auth=is_external_auth, ip_addr=ip_addr)
-    if auth_user is None:
-        return None
-
-    user.update_lastlogin()
-    meta.Session().commit()
-
-    # Start new session to prevent session fixation attacks.
-    session.invalidate()
-    session['authuser'] = cookie = auth_user.to_cookie()
-
-    # If they want to be remembered, update the cookie.
-    # NOTE: Assumes that beaker defaults to browser session cookie.
-    if remember:
-        t = datetime.datetime.now() + datetime.timedelta(days=365)
-        session._set_cookie_expires(t)
-
-    session.save()
-
-    log.info('user %s is now authenticated and stored in '
-             'session, session attrs %s', user.username, cookie)
-
-    # dumps session attrs back to cookie
-    session._update_cookie_out()
-
-    return auth_user
-
-
-class BasicAuth(paste.auth.basic.AuthBasicAuthenticator):
-
-    def __init__(self, realm, authfunc, auth_http_code=None):
-        self.realm = realm
-        self.authfunc = authfunc
-        self._rc_auth_http_code = auth_http_code
-
-    def build_authentication(self, environ):
-        head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
-        # Consume the whole body before sending a response
-        try:
-            request_body_size = int(environ.get('CONTENT_LENGTH', 0))
-        except (ValueError):
-            request_body_size = 0
-        environ['wsgi.input'].read(request_body_size)
-        if self._rc_auth_http_code and self._rc_auth_http_code == '403':
-            # return 403 if alternative http return code is specified in
-            # Kallithea config
-            return paste.httpexceptions.HTTPForbidden(headers=head)
-        return paste.httpexceptions.HTTPUnauthorized(headers=head)
-
-    def authenticate(self, environ):
-        authorization = paste.httpheaders.AUTHORIZATION(environ)
-        if not authorization:
-            return self.build_authentication(environ)
-        (authmeth, auth) = authorization.split(' ', 1)
-        if 'basic' != authmeth.lower():
-            return self.build_authentication(environ)
-        auth = safe_str(base64.b64decode(auth.strip()))
-        _parts = auth.split(':', 1)
-        if len(_parts) == 2:
-            username, password = _parts
-            if self.authfunc(username, password, environ) is not None:
-                return username
-        return self.build_authentication(environ)
-
-    __call__ = authenticate
-
-
-class BaseVCSController(object):
-    """Base controller for handling Mercurial/Git protocol requests
-    (coming from a VCS client, and not a browser).
-    """
-
-    scm_alias = None # 'hg' / 'git'
-
-    def __init__(self, application, config):
-        self.application = application
-        self.config = config
-        # base path of repo locations
-        self.basepath = self.config['base_path']
-        # authenticate this VCS request using the authentication modules
-        self.authenticate = BasicAuth('', auth_modules.authenticate,
-                                      config.get('auth_ret_code'))
-
-    @classmethod
-    def parse_request(cls, environ):
-        """If request is parsed as a request for this VCS, return a namespace with the parsed request.
-        If the request is unknown, return None.
-        """
-        raise NotImplementedError()
-
-    def _authorize(self, environ, action, repo_name, ip_addr):
-        """Authenticate and authorize user.
-
-        Since we're dealing with a VCS client and not a browser, we only
-        support HTTP basic authentication, either directly via raw header
-        inspection, or by using container authentication to delegate the
-        authentication to the web server.
-
-        Returns (user, None) on successful authentication and authorization.
-        Returns (None, wsgi_app) to send the wsgi_app response to the client.
-        """
-        # Use anonymous access if allowed for action on repo.
-        default_user = db.User.get_default_user()
-        default_authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
-        if default_authuser is None:
-            log.debug('No anonymous access at all') # move on to proper user auth
-        else:
-            if self._check_permission(action, default_authuser, repo_name):
-                return default_authuser, None
-            log.debug('Not authorized to access this repository as anonymous user')
-
-        username = None
-        #==============================================================
-        # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
-        # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
-        #==============================================================
-
-        # try to auth based on environ, container auth methods
-        log.debug('Running PRE-AUTH for container based authentication')
-        pre_auth = auth_modules.authenticate('', '', environ)
-        if pre_auth is not None and pre_auth.get('username'):
-            username = pre_auth['username']
-        log.debug('PRE-AUTH got %s as username', username)
-
-        # If not authenticated by the container, running basic auth
-        if not username:
-            self.authenticate.realm = self.config['realm']
-            result = self.authenticate(environ)
-            if isinstance(result, str):
-                paste.httpheaders.AUTH_TYPE.update(environ, 'basic')
-                paste.httpheaders.REMOTE_USER.update(environ, result)
-                username = result
-            else:
-                return None, result.wsgi_application
-
-        #==============================================================
-        # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
-        #==============================================================
-        try:
-            user = db.User.get_by_username_or_email(username)
-        except Exception:
-            log.error(traceback.format_exc())
-            return None, webob.exc.HTTPInternalServerError()
-
-        authuser = AuthUser.make(dbuser=user, ip_addr=ip_addr)
-        if authuser is None:
-            return None, webob.exc.HTTPForbidden()
-        if not self._check_permission(action, authuser, repo_name):
-            return None, webob.exc.HTTPForbidden()
-
-        return user, None
-
-    def _handle_request(self, environ, start_response):
-        raise NotImplementedError()
-
-    def _check_permission(self, action, authuser, repo_name):
-        """
-        :param action: 'push' or 'pull'
-        :param user: `AuthUser` instance
-        :param repo_name: repository name
-        """
-        if action == 'push':
-            if not HasPermissionAnyMiddleware('repository.write',
-                                              'repository.admin')(authuser,
-                                                                  repo_name):
-                return False
-
-        elif action == 'pull':
-            #any other action need at least read permission
-            if not HasPermissionAnyMiddleware('repository.read',
-                                              'repository.write',
-                                              'repository.admin')(authuser,
-                                                                  repo_name):
-                return False
-
-        else:
-            assert False, action
-
-        return True
-
-    def __call__(self, environ, start_response):
-        try:
-            # try parsing a request for this VCS - if it fails, call the wrapped app
-            parsed_request = self.parse_request(environ)
-            if parsed_request is None:
-                return self.application(environ, start_response)
-
-            # skip passing error to error controller
-            environ['pylons.status_code_redirect'] = True
-
-            # quick check if repo exists...
-            if not is_valid_repo(parsed_request.repo_name, self.basepath, self.scm_alias):
-                raise webob.exc.HTTPNotFound()
-
-            if parsed_request.action is None:
-                # Note: the client doesn't get the helpful error message
-                raise webob.exc.HTTPBadRequest('Unable to detect pull/push action for %r! Are you using a nonstandard command or client?' % parsed_request.repo_name)
-
-            #======================================================================
-            # CHECK PERMISSIONS
-            #======================================================================
-            ip_addr = get_ip_addr(environ)
-            user, response_app = self._authorize(environ, parsed_request.action, parsed_request.repo_name, ip_addr)
-            if response_app is not None:
-                return response_app(environ, start_response)
-
-            #======================================================================
-            # REQUEST HANDLING
-            #======================================================================
-            set_hook_environment(user.username, ip_addr,
-                parsed_request.repo_name, self.scm_alias, parsed_request.action)
-
-            try:
-                log.info('%s action on %s repo "%s" by "%s" from %s',
-                         parsed_request.action, self.scm_alias, parsed_request.repo_name, user.username, ip_addr)
-                app = self._make_app(parsed_request)
-                return app(environ, start_response)
-            except Exception:
-                log.error(traceback.format_exc())
-                raise webob.exc.HTTPInternalServerError()
-
-        except webob.exc.HTTPException as e:
-            return e(environ, start_response)
-
-
-class BaseController(TGController):
-
-    def _before(self, *args, **kwargs):
-        """
-        _before is called before controller methods and after __call__
-        """
-        if request.needs_csrf_check:
-            # CSRF protection: Whenever a request has ambient authority (whether
-            # through a session cookie or its origin IP address), it must include
-            # the correct token, unless the HTTP method is GET or HEAD (and thus
-            # guaranteed to be side effect free. In practice, the only situation
-            # where we allow side effects without ambient authority is when the
-            # authority comes from an API key; and that is handled above.
-            token = request.POST.get(webutils.session_csrf_secret_name)
-            if not token or token != webutils.session_csrf_secret_token():
-                log.error('CSRF check failed')
-                raise webob.exc.HTTPForbidden()
-
-        c.kallithea_version = kallithea.__version__
-        settings = db.Setting.get_app_settings()
-
-        # Visual options
-        c.visual = AttributeDict({})
-
-        ## DB stored
-        c.visual.show_public_icon = asbool(settings.get('show_public_icon'))
-        c.visual.show_private_icon = asbool(settings.get('show_private_icon'))
-        c.visual.stylify_metalabels = asbool(settings.get('stylify_metalabels'))
-        c.visual.page_size = safe_int(settings.get('dashboard_items', 100))
-        c.visual.admin_grid_items = safe_int(settings.get('admin_grid_items', 100))
-        c.visual.repository_fields = asbool(settings.get('repository_fields'))
-        c.visual.show_version = asbool(settings.get('show_version'))
-        c.visual.use_gravatar = asbool(settings.get('use_gravatar'))
-        c.visual.gravatar_url = settings.get('gravatar_url')
-
-        c.ga_code = settings.get('ga_code')
-        # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code
-        if c.ga_code and '<' not in c.ga_code:
-            c.ga_code = '''<script type="text/javascript">
-                var _gaq = _gaq || [];
-                _gaq.push(['_setAccount', '%s']);
-                _gaq.push(['_trackPageview']);
-
-                (function() {
-                    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
-                    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
-                    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
-                    })();
-            </script>''' % c.ga_code
-        c.site_name = settings.get('title')
-        c.clone_uri_tmpl = settings.get('clone_uri_tmpl') or db.Repository.DEFAULT_CLONE_URI
-        c.clone_ssh_tmpl = settings.get('clone_ssh_tmpl') or db.Repository.DEFAULT_CLONE_SSH
-
-        ## INI stored
-        c.visual.allow_repo_location_change = asbool(config.get('allow_repo_location_change', True))
-        c.visual.allow_custom_hooks_settings = asbool(config.get('allow_custom_hooks_settings', True))
-        c.ssh_enabled = asbool(config.get('ssh_enabled', False))
-
-        c.instance_id = config.get('instance_id')
-        c.issues_url = config.get('bugtracker', url('issues_url'))
-        # END CONFIG VARS
-
-        c.repo_name = get_repo_slug(request)  # can be empty
-        c.backends = list(kallithea.BACKENDS)
-
-        self.cut_off_limit = safe_int(config.get('cut_off_limit'))
-
-        c.my_pr_count = db.PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count()
-
-        self.scm_model = ScmModel()
-
-    @staticmethod
-    def _determine_auth_user(session_authuser, ip_addr):
-        """
-        Create an `AuthUser` object given the API key/bearer token
-        (if any) and the value of the authuser session cookie.
-        Returns None if no valid user is found (like not active or no access for IP).
-        """
-
-        # Authenticate by session cookie
-        # In ancient login sessions, 'authuser' may not be a dict.
-        # In that case, the user will have to log in again.
-        # v0.3 and earlier included an 'is_authenticated' key; if present,
-        # this must be True.
-        if isinstance(session_authuser, dict) and session_authuser.get('is_authenticated', True):
-            return AuthUser.from_cookie(session_authuser, ip_addr=ip_addr)
-
-        # Authenticate by auth_container plugin (if enabled)
-        if any(
-            plugin.is_container_auth
-            for plugin in auth_modules.get_auth_plugins()
-        ):
-            try:
-                user_info = auth_modules.authenticate('', '', request.environ)
-            except UserCreationError as e:
-                webutils.flash(e, 'error', logf=log.error)
-            else:
-                if user_info is not None:
-                    username = user_info['username']
-                    user = db.User.get_by_username(username, case_insensitive=True)
-                    return log_in_user(user, remember=False, is_external_auth=True, ip_addr=ip_addr)
-
-        # User is default user (if active) or anonymous
-        default_user = db.User.get_default_user()
-        authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
-        if authuser is None: # fall back to anonymous
-            authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make?
-        return authuser
-
-    @staticmethod
-    def _basic_security_checks():
-        """Perform basic security/sanity checks before processing the request."""
-
-        # Only allow the following HTTP request methods.
-        if request.method not in ['GET', 'HEAD', 'POST']:
-            raise webob.exc.HTTPMethodNotAllowed()
-
-        # Also verify the _method override - no longer allowed.
-        if request.params.get('_method') is None:
-            pass # no override, no problem
-        else:
-            raise webob.exc.HTTPMethodNotAllowed()
-
-        # Make sure CSRF token never appears in the URL. If so, invalidate it.
-        if webutils.session_csrf_secret_name in request.GET:
-            log.error('CSRF key leak detected')
-            session.pop(webutils.session_csrf_secret_name, None)
-            session.save()
-            webutils.flash(_('CSRF token leak has been detected - all form tokens have been expired'),
-                    category='error')
-
-        # WebOb already ignores request payload parameters for anything other
-        # than POST/PUT, but double-check since other Kallithea code relies on
-        # this assumption.
-        if request.method not in ['POST', 'PUT'] and request.POST:
-            log.error('%r request with payload parameters; WebOb should have stopped this', request.method)
-            raise webob.exc.HTTPBadRequest()
-
-    def __call__(self, environ, context):
-        try:
-            ip_addr = get_ip_addr(environ)
-            self._basic_security_checks()
-
-            api_key = request.GET.get('api_key')
-            try:
-                # Request.authorization may raise ValueError on invalid input
-                type, params = request.authorization
-            except (ValueError, TypeError):
-                pass
-            else:
-                if type.lower() == 'bearer':
-                    api_key = params # bearer token is an api key too
-
-            if api_key is None:
-                authuser = self._determine_auth_user(
-                    session.get('authuser'),
-                    ip_addr=ip_addr,
-                )
-                needs_csrf_check = request.method not in ['GET', 'HEAD']
-
-            else:
-                dbuser = db.User.get_by_api_key(api_key)
-                if dbuser is None:
-                    log.info('No db user found for authentication with API key ****%s from %s',
-                             api_key[-4:], ip_addr)
-                authuser = AuthUser.make(dbuser=dbuser, is_external_auth=True, ip_addr=ip_addr)
-                needs_csrf_check = False # API key provides CSRF protection
-
-            if authuser is None:
-                log.info('No valid user found')
-                raise webob.exc.HTTPForbidden()
-
-            # set globals for auth user
-            request.authuser = authuser
-            request.ip_addr = ip_addr
-            request.needs_csrf_check = needs_csrf_check
-
-            log.info('IP: %s User: %s Request: %s',
-                request.ip_addr, request.authuser,
-                get_path_info(environ),
-            )
-            return super(BaseController, self).__call__(environ, context)
-        except webob.exc.HTTPException as e:
-            return e
-
-
-class BaseRepoController(BaseController):
-    """
-    Base class for controllers responsible for loading all needed data for
-    repository loaded items are
-
-    c.db_repo_scm_instance: instance of scm repository
-    c.db_repo: instance of db
-    c.repository_followers: number of followers
-    c.repository_forks: number of forks
-    c.repository_following: weather the current user is following the current repo
-    """
-
-    def _before(self, *args, **kwargs):
-        super(BaseRepoController, self)._before(*args, **kwargs)
-        if c.repo_name:  # extracted from request by base-base BaseController._before
-            _dbr = db.Repository.get_by_repo_name(c.repo_name)
-            if not _dbr:
-                return
-
-            log.debug('Found repository in database %s with state `%s`',
-                      _dbr, _dbr.repo_state)
-            route = getattr(request.environ.get('routes.route'), 'name', '')
-
-            # allow to delete repos that are somehow damages in filesystem
-            if route in ['delete_repo']:
-                return
-
-            if _dbr.repo_state in [db.Repository.STATE_PENDING]:
-                if route in ['repo_creating_home']:
-                    return
-                check_url = url('repo_creating_home', repo_name=c.repo_name)
-                raise webob.exc.HTTPFound(location=check_url)
-
-            dbr = c.db_repo = _dbr
-            c.db_repo_scm_instance = c.db_repo.scm_instance
-            if c.db_repo_scm_instance is None:
-                log.error('%s this repository is present in database but it '
-                          'cannot be created as an scm instance', c.repo_name)
-                webutils.flash(_('Repository not found in the filesystem'),
-                        category='error')
-                raise webob.exc.HTTPNotFound()
-
-            # some globals counter for menu
-            c.repository_followers = self.scm_model.get_followers(dbr)
-            c.repository_forks = self.scm_model.get_forks(dbr)
-            c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
-            c.repository_following = self.scm_model.is_following_repo(
-                                    c.repo_name, request.authuser.user_id)
-
-    @staticmethod
-    def _get_ref_rev(repo, ref_type, ref_name, returnempty=False):
-        """
-        Safe way to get changeset. If error occurs show error.
-        """
-        try:
-            return repo.scm_instance.get_ref_revision(ref_type, ref_name)
-        except EmptyRepositoryError as e:
-            if returnempty:
-                return repo.scm_instance.EMPTY_CHANGESET
-            webutils.flash(_('There are no changesets yet'), category='error')
-            raise webob.exc.HTTPNotFound()
-        except ChangesetDoesNotExistError as e:
-            webutils.flash(_('Changeset for %s %s not found in %s') %
-                              (ref_type, ref_name, repo.repo_name),
-                    category='error')
-            raise webob.exc.HTTPNotFound()
-        except RepositoryError as e:
-            log.error(traceback.format_exc())
-            webutils.flash(e, category='error')
-            raise webob.exc.HTTPBadRequest()
-
-
-@decorator.decorator
-def jsonify(func, *args, **kwargs):
-    """Action decorator that formats output for JSON
-
-    Given a function that will return content, this decorator will turn
-    the result into JSON, with a content-type of 'application/json' and
-    output it.
-    """
-    response.headers['Content-Type'] = 'application/json; charset=utf-8'
-    data = func(*args, **kwargs)
-    if isinstance(data, (list, tuple)):
-        # A JSON list response is syntactically valid JavaScript and can be
-        # loaded and executed as JavaScript by a malicious third-party site
-        # using <script>, which can lead to cross-site data leaks.
-        # JSON responses should therefore be scalars or objects (i.e. Python
-        # dicts), because a JSON object is a syntax error if intepreted as JS.
-        msg = "JSON responses with Array envelopes are susceptible to " \
-              "cross-site data leak attacks, see " \
-              "https://web.archive.org/web/20120519231904/http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
-        warnings.warn(msg, Warning, 2)
-        log.warning(msg)
-    log.debug("Returning JSON wrapped action output")
-    return ascii_bytes(ext_json.dumps(data))
-
-@decorator.decorator
-def IfSshEnabled(func, *args, **kwargs):
-    """Decorator for functions that can only be called if SSH access is enabled.
-
-    If SSH access is disabled in the configuration file, HTTPNotFound is raised.
-    """
-    if not c.ssh_enabled:
-        webutils.flash(_("SSH access is disabled."), category='warning')
-        raise webob.exc.HTTPNotFound()
-    return func(*args, **kwargs)
--- a/scripts/deps.py	Mon Dec 14 23:00:25 2020 +0100
+++ b/scripts/deps.py	Fri Nov 13 01:06:16 2020 +0100
@@ -131,10 +131,10 @@
 
 normal_modules = set('''
 kallithea
+kallithea.controllers.base
 kallithea.lib
 kallithea.lib.auth
 kallithea.lib.auth_modules
-kallithea.lib.base
 kallithea.lib.celerylib
 kallithea.lib.db_manage
 kallithea.lib.helpers