Browse Source

More services moved to new architecture

Ralfp 12 years ago
parent
commit
089566956c

+ 0 - 12
misago/acl/decorators.py

@@ -1,12 +0,0 @@
-from misago.acl.exceptions import ACLError403, ACLError404
-from misago.shared.views import error403, error404
-
-def acl_errors(f):
-    def decorator(*args, **kwargs):
-        try:
-            return f(*args, **kwargs)
-        except ACLError403 as e:
-            return error403(args[0], e.message)
-        except ACLError404 as e:
-            return error404(args[0], e.message)
-    return decorator

+ 17 - 2
misago/context_processors.py

@@ -1,13 +1,28 @@
+from django.conf import settings
 from misago import __version__
+from misago.admin import site
 
-def misago(request):
+def common(request):
     try:
         return {
-            'version': __version__,
+            'acl': request.acl,
+            'board_address': settings.BOARD_ADDRESS,
+            'csrf_id': request.csrf.csrf_id,
+            'csrf_token': request.csrf.csrf_token,
+            'is_banned': request.ban.is_banned(),
+            'is_jammed': request.jam.is_jammed(),
+            'messages' : request.messages.messages,
             'monitor': request.monitor,
+            'request_path': request.get_full_path(),
             'settings': request.settings,
             'stopwatch': request.stopwatch.time(),
+            'user': request.user,
+            'version': __version__,
         }
     except AttributeError:
         # If request lacks required service, let template crash in context processor's stead
         return  {}
+
+
+def admin(request):
+    site.get_admin_navigation(request)

+ 39 - 0
misago/crawlers.py

@@ -0,0 +1,39 @@
+CRAWLERS_NAMES = {
+    'bing': 'Bingbot',
+    'google': 'Googlebot',
+    'yahoo': 'Yahoo! Slurp',
+    'yahooch': 'Yahoo! Slurp China',
+}
+
+CRAWLERS_AGENTS = {
+    'bingbot/': 'bing',
+    'Googlebot/': 'google',
+    'Yahoo! Slurp China': 'yahooch',
+    'Yahoo! Slurp': 'yahoo',
+}
+
+CRAWLERS_HOSTS = {
+}
+
+
+class Crawler(object):
+    crawler = False
+    host = None
+    username = None
+    
+    def __init__(self, agent = None, ip = None):
+        if agent is not None:
+            for item in CRAWLERS_AGENTS.keys():
+            	if agent.find(item) != -1:
+                    self.crawler = True
+                    self.username = CRAWLERS_AGENTS[item]
+                    
+        if ip is not None:
+            for item in CRAWLERS_HOSTS.keys():
+            	if ip == item:
+                    self.crawler = True
+                    self.username = CRAWLERS_HOSTS[item]
+                    
+        if self.crawler:
+            self.username = CRAWLERS_NAMES[self.username]
+            self.host = ip

+ 78 - 0
misago/decorators.py

@@ -0,0 +1,78 @@
+from django.utils.translation import ugettext as _
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.shared.views import error403, error404, error_banned
+
+def acl_errors(f):
+    def decorator(*args, **kwargs):
+        try:
+            return f(*args, **kwargs)
+        except ACLError403 as e:
+            return error403(args[0], e.message)
+        except ACLError404 as e:
+            return error404(args[0], e.message)
+    return decorator
+
+
+def block_authenticated(f):
+    def decorator(*args, **kwargs):
+        request = args[0]
+        if not request.firewall.admin and request.user.is_authenticated():
+            from misago.views import error403
+            return error403(request, _("%(username)s, this page is not available to signed in users.") % {'username': request.user.username})
+        return f(*args, **kwargs)
+    return decorator
+
+
+def block_banned(f):
+    def decorator(*args, **kwargs):
+        request = args[0]
+        try:
+            if request.ban.is_banned():
+                from misago.banning.views import error_banned
+                return error_banned(request);
+            return f(*args, **kwargs)
+        except AttributeError:
+            pass
+        return f(*args, **kwargs)
+    return decorator
+
+
+def block_crawlers(f):
+    def decorator(*args, **kwargs):
+        request = args[0]
+        if request.user.is_crawler():
+            return error403(request)
+        return f(*args, **kwargs)
+    return decorator
+
+
+def block_guest(f):
+    def decorator(*args, **kwargs):
+        request = args[0]
+        if not request.user.is_authenticated():
+            from misago.views import error403
+            return error403(request, _("Dear Guest, only signed in members are allowed to access this page. Please sign in or register and try again."))
+        return f(*args, **kwargs)
+    return decorator
+
+
+def block_jammed(f):
+    def decorator(*args, **kwargs):
+        request = args[0]
+        try:
+            if not request.firewall.admin and request.jam.is_jammed():
+                return error403(request, _("You have used up allowed attempts quota and we temporarily banned you from accessing this page."))
+        except AttributeError:
+            pass
+        return f(*args, **kwargs)
+    return decorator
+
+
+def check_csrf(f):
+    def decorator(*args, **kwargs):
+        request = args[0]
+        if not request.csrf.request_secure(request):
+            from misago.views import error403
+            return error403(request, _("Request authorization is invalid. Please try again."))
+        return f(*args, **kwargs)
+    return decorator

+ 37 - 0
misago/firewalls.py

@@ -0,0 +1,37 @@
+from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
+from misago.admin import ADMIN_PATH
+from misago.messages import Message
+from misago.shared.views import error403, error404, signin
+
+class FirewallForum(object):
+    admin = False
+    prefix = ''
+
+    def behind_firewall(self, path):
+        """
+        Firewall test, it checks if requested path is behind firewall
+        """
+        return path[:len(self.prefix)] == self.prefix
+
+    def process_view(self, request, callback, callback_args, callback_kwargs):
+        return None
+
+
+class FirewallAdmin(FirewallForum):
+    admin = True
+    prefix = '/' + ADMIN_PATH
+
+    def process_view(self, request, callback, callback_args, callback_kwargs):
+        # Block all crawlers with 403
+        if request.user.is_crawler():
+            request.theme.reset_theme()
+            return error403(request)
+        else:
+            # If we are not authenticated or not admin, force us to sign in right way
+            if not request.user.is_authenticated():
+                return signin(request)
+            elif not request.user.is_god() and not request.acl.admin.is_admin():
+                request.messages.set_message(Message(_("Your account does not have admin privileges")), 'error', 'security')
+                return signin(request)
+            return None

+ 12 - 0
misago/management/commands/clearattempts.py

@@ -0,0 +1,12 @@
+from datetime import timedelta
+from django.utils import timezone
+from misago.bruteforce.models import SignInAttempt
+
+class Command(BaseCommand):
+    """
+    This command is intended to work as CRON job fired every few days to remove failed sign-in attempts
+    """
+    help = 'Clears sign-in attempts log'
+    def handle(self):
+        SignInAttempt.objects.filter(date__lte=timezone.now() - timedelta(hours=24)).delete()
+        self.stdout.write('Failed Sign-In attempts older than 24h have been removed.\n')

+ 39 - 0
misago/messages.py

@@ -0,0 +1,39 @@
+class Messages(object):
+    def __init__(self, session):
+        self.session = session
+        self.messages = session.get('messages_list', [])
+        self.session['messages_list'] = []
+
+    def set_message(self, message, type='info', owner=None):
+        message.type = type
+        message.owner = owner
+        self.messages.append(message)
+
+    def set_flash(self, message, type='info', owner=None):
+        self.set_message(message, type, owner)
+        self.session['messages_list'].append(message)
+
+    def get_message(self, owner=None):
+        for index, message in enumerate(self.messages):
+            if message.owner == owner:
+                del self.messages[index]
+                return message
+        return None
+
+    def get_messages(self, owner=None):
+        orphans = []
+        messages = []
+        for message in self.messages:
+            if message.owner == owner:
+                messages.append(message)
+            else:
+                orphans.append(message)
+        self.messages = orphans
+        return messages
+
+
+class Message(object):
+    def __init__(self, message=None, type='info', owner=None):
+        self.type = type
+        self.message = message
+        self.owner = owner

+ 1 - 0
misago/middleware/acl.py

@@ -8,6 +8,7 @@ class ACLMiddleware(object):
             (request.acl.team or request.user.is_god()) != request.user.is_team):
             request.user.is_team = (request.acl.team or request.user.is_god())
             request.user.save(force_update=True)
+            
         if request.session.team != request.user.is_team:
             request.session.team = request.user.is_team
             request.session.save()

+ 31 - 0
misago/middleware/bruteforce.py

@@ -0,0 +1,31 @@
+from datetime import timedelta
+from django.utils import timezone
+from misago.models import SignInAttempt
+
+class JamCache(object):
+    def __init__(self):
+        jammed = False
+        expires = timezone.now()
+    
+    def check_for_updates(self, request):
+        if self.expires < timezone.now():
+            self.jammed = SignInAttempt.objects.is_jammed(request.settings, request.session.get_ip(request))
+            self.expires = timezone.now() + timedelta(minutes=request.settings['jams_lifetime'])
+            return True
+        return False
+
+    def is_jammed(self):
+        return self.jammed
+
+
+class JamMiddleware(object):
+    def process_request(self, request):
+        if request.user.is_crawler():
+            return None
+        try:
+            request.jam = request.session['jam']
+        except KeyError:
+            request.jam = JamCache()
+            request.session['jam'] = request.jam
+        if not request.firewall.admin:
+            request.jam.check_for_updates(request)

+ 15 - 0
misago/middleware/crawlers.py

@@ -0,0 +1,15 @@
+from misago.crawlers import Crawler
+from misago import models
+
+class DetectCrawlerMiddleware(object):
+    def process_request(self, request):
+        # If its correct request (We have client IP), see if it exists in Crawlers DB
+        if request.META.get('HTTP_X_FORWARDED_FOR') or request.META.get('REMOTE_ADDR'):
+            found_crawler = Crawler(
+                                    request.META.get('HTTP_USER_AGENT', ''),
+                                    request.META.get('HTTP_X_FORWARDED_FOR') or request.META.get('REMOTE_ADDR')
+                                    )
+            
+            # If crawler exists in database, use it as this request user
+            if found_crawler.crawler:
+                request.user = models.Crawler(found_crawler.username)

+ 23 - 0
misago/middleware/csrf.py

@@ -0,0 +1,23 @@
+from misago.utils.strings import random_string
+
+class CSRFProtection(object):
+    def __init__(self, csrf_token):
+        self.csrf_id = '_csrf_token'
+        self.csrf_token = csrf_token
+        
+    def request_secure(self, request):
+        return request.method == 'POST' and request.POST.get(self.csrf_id) == self.csrf_token
+
+
+class CSRFMiddleware(object):
+    def process_request(self, request):
+        if request.user.is_crawler():
+            return None
+
+        if 'csrf_token' in request.session:
+            csrf_token = request.session['csrf_token']
+        else:
+            csrf_token = random_string(16);
+            request.session['csrf_token'] = csrf_token
+        
+        request.csrf = CSRFProtection(csrf_token)

+ 17 - 0
misago/middleware/firewalls.py

@@ -0,0 +1,17 @@
+from django.conf import settings
+from misago.firewalls import *
+from misago.theme import Theme
+
+class FirewallMiddleware(object):
+    firewall_admin = FirewallAdmin()
+    firewall_forum = FirewallForum()
+
+    def process_request(self, request):
+        if settings.ADMIN_PATH and self.firewall_admin.behind_firewall(request.path_info):
+            request.firewall = self.firewall_admin
+            request.theme.set_theme('admin')
+        else:
+            request.firewall = self.firewall_forum
+
+    def process_view(self, request, callback, callback_args, callback_kwargs):
+        return request.firewall.process_view(request, callback, callback_args, callback_kwargs)

+ 5 - 0
misago/middleware/messages.py

@@ -0,0 +1,5 @@
+from misago.messages import Messages
+
+class MessagesMiddleware(object):
+    def process_request(self, request):
+        request.messages = Messages(request.session)

+ 24 - 0
misago/middleware/theme.py

@@ -0,0 +1,24 @@
+from django.conf import settings
+from django.core.cache import cache
+from misago.theme import Theme
+from misago.models import ThemeAdjustment
+
+class ThemeMiddleware(object):
+    def process_request(self, request):
+        if not settings.INSTALLED_THEMES:
+            raise ValueError('There are no themes installed!')
+        request.theme = Theme(settings.INSTALLED_THEMES[0])
+        
+        # Adjust theme for specific client?
+        if request.META.get('HTTP_USER_AGENT'):
+            adjustments = cache.get('client_adjustments', 'nada')
+            if adjustments == 'nada':
+                adjustments = ThemeAdjustment.objects.all()
+                cache.set('client_adjustments', adjustments)
+            if adjustments:
+                user_agent = request.META.get('HTTP_USER_AGENT').lower()
+                for item in adjustments:
+                    if item.adjust_theme(user_agent):
+                        request.theme = Theme(item.theme)
+                        break
+            

+ 28 - 0
misago/middleware/user.py

@@ -0,0 +1,28 @@
+from django.conf import settings
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
+from misago.messages import Message
+
+def set_timezone(new_tz):
+    if settings.USE_TZ:
+        try:
+            import pytz
+            timezone.activate(pytz.timezone(new_tz))
+        except ImportError:
+            pass
+
+
+class UserMiddleware(object):
+    def process_request(self, request):
+        if request.user.is_authenticated():
+            # Set user timezone and rank
+            request.session.rank = request.user.rank_id
+            set_timezone(request.user.timezone)
+
+            # Display "welcome back!" message
+            if request.session.remember_me:
+                request.messages.set_message(Message(_("Welcome back, %(username)s! We've signed you in automatically for your convenience.") % {'username': request.user.username}), 'info')
+        else:
+            # Set guest's timezone and empty rank
+            set_timezone(request.settings['default_timezone'])
+            request.session.rank = None

+ 2 - 1
misago/models/__init__.py

@@ -22,4 +22,5 @@ from misago.models.threadmodel import Thread
 from misago.models.threadreadmodel import ThreadRead
 from misago.models.tokenmodel import Token
 from misago.models.usermodel import User, Guest, Crawler
-from misago.models.usernamechangemodel import UsernameChange
+from misago.models.usernamechangemodel import UsernameChange
+from misago.models.watchedthreadmodel import WatchedThread

+ 3 - 0
misago/models/usermodel.py

@@ -185,6 +185,9 @@ class User(models.Model):
 
     statistics_name = _('Users Registrations')
 
+    class Meta:
+        app_label = 'misago'
+
     def is_god(self):
         try:
             return self.is_god_cache

+ 0 - 1
misago/models/watchedthreadmodel.py

@@ -1,5 +1,4 @@
 from django.db import models
-from misago.forums.signals import 
 from misago.signals import merge_thread, move_forum_content, move_thread
 
 class WatchedThread(models.Model):

+ 10 - 18
misago/settings_base.py

@@ -75,16 +75,8 @@ TEMPLATE_CONTEXT_PROCESSORS = (
     'django.core.context_processors.static',
     'django.core.context_processors.tz',
     'django.contrib.messages.context_processors.messages',
-    'misago.context_processors.misago',
-    'misago.admin.context_processors.admin',
-    'misago.banning.context_processors.banning',
-    'misago.messages.context_processors.messages',
-    'misago.monitor.context_processors.monitor',
-    'misago.settings.context_processors.settings',
-    'misago.bruteforce.context_processors.is_jammed',
-    'misago.csrf.context_processors.csrf',
-    'misago.users.context_processors.user',
-    'misago.acl.context_processors.acl',
+    'misago.context_processors.common',
+    'misago.context_processors.admin',
 )
 
 # Jinja2 Template Extensions
@@ -100,16 +92,16 @@ MIDDLEWARE_CLASSES = (
     'misago.middleware.cookiejar.CookieJarMiddleware',
     'misago.middleware.settings.SettingsMiddleware',
     'misago.middleware.monitor.MonitorMiddleware',
-    #'misago.themes.middleware.ThemeMiddleware',
-    #'misago.firewalls.middleware.FirewallMiddleware',
-    #'misago.crawlers.middleware.DetectCrawlerMiddleware',
+    'misago.middleware.theme.ThemeMiddleware',
+    'misago.middleware.firewalls.FirewallMiddleware',
+    'misago.middleware.crawlers.DetectCrawlerMiddleware',
     'misago.middleware.session.SessionMiddleware',
-    #'misago.bruteforce.middleware.JamMiddleware',
-    #'misago.csrf.middleware.CSRFMiddleware',
+    'misago.middleware.bruteforce.JamMiddleware',
+    'misago.middleware.csrf.CSRFMiddleware',
     'misago.middleware.banning.BanningMiddleware',
-    #'misago.messages.middleware.MessagesMiddleware',
-    #'misago.users.middleware.UserMiddleware',
-    #'misago.acl.middleware.ACLMiddleware',
+    'misago.middleware.messages.MessagesMiddleware',
+    'misago.middleware.users.UserMiddleware',
+    'misago.middleware.acl.ACLMiddleware',
     'django.middleware.common.CommonMiddleware',
 )
 

+ 0 - 0
misago/validators/__init__.py → misago/shared/forms.py


+ 15 - 0
misago/shared/views.py

@@ -31,3 +31,18 @@ def error403(request, message=None):
 
 def error404(request, message=None):
     return error_view(request, 404, message)
+
+
+def error_banned(request, user=None, ban=None):
+    if not ban:
+        ban = request.ban
+    response = request.theme.render_to_response('error403_banned.html',
+                                                {
+                                                 'banned_user': user,
+                                                 'ban': ban,
+                                                 'hide_signin': True,
+                                                 'exception_response': True,
+                                                 },
+                                                context_instance=RequestContext(request));
+    response.status_code = 403
+    return response

+ 55 - 0
misago/theme.py

@@ -0,0 +1,55 @@
+from django.conf import settings
+from coffin.shortcuts import render, render_to_response
+from coffin.template.loader import get_template, select_template, render_to_string
+
+'''Monkeypatch Django to mimic Jinja2 behaviour'''
+from django.utils import safestring
+if not hasattr(safestring, '__html__'):
+    safestring.SafeString.__html__ = lambda self: str(self)
+    safestring.SafeUnicode.__html__ = lambda self: unicode(self)
+
+class Theme(object):
+    def __init__(self, theme):
+        self.set_theme(theme);
+
+    def set_theme(self, theme):
+        if theme not in settings.INSTALLED_THEMES:
+            raise ValueError('"%s" is not correct theme name.' % theme)
+        if theme[0] == '_':
+            raise ValueError('"%s" is a template package, not a theme.' % theme[1:])
+        self._theme = theme;
+
+    def reset_theme(self):
+        self._theme = settings.INSTALLED_THEMES[0]
+
+    def get_theme(self):
+        return self._theme
+
+    def prefix_templates(self, templates):
+        if isinstance(templates, str):
+            return ('%s/%s' % (self._theme, templates), templates)
+        else:
+            prefixed = []
+            for template in templates:
+                prefixed.append('%s/%s' % (self._theme, template))
+            prefixed += templates
+            return prefixed
+
+    def render(self, request, *args, **kwargs):
+        return render(request, *args, **kwargs)
+
+    def render_to_string(self, templates, *args, **kwargs):
+        templates = self.prefix_templates(templates)
+        return render_to_string(templates, *args, **kwargs)
+
+    def render_to_response(self, templates, *args, **kwargs):
+        templates = self.prefix_templates(templates)
+        return render_to_response(templates, *args, **kwargs)
+
+    def get_email_templates(self, template, contex={}):
+            email_type_plain = '_email/%s_plain.html' % template
+            email_type_html = '_email/%s_html.html' % template
+            return (
+                    select_template(('%s/%s' % (self._theme, email_type_plain[1:]), email_type_plain)),
+                    select_template(('%s/%s' % (self._theme, email_type_html[1:]), email_type_html)),
+                    )

+ 6 - 3
misago/validators.py

@@ -3,9 +3,6 @@ from django.conf import settings
 from django.core.exceptions import ValidationError
 from django.utils.translation import ungettext, ugettext_lazy as _
 from misago.models import Ban
-from misago.settings import DBSettings
-from django.core.exceptions import ValidationError
-from django.utils.translation import ugettext_lazy as _
 from misago.utils.strings import slugify
 
 class validate_sluggable(object):
@@ -23,6 +20,7 @@ class validate_sluggable(object):
 
 def validate_username(value, db_settings):
     value = unicode(value).strip()
+
     if len(value) < db_settings['username_length_min']:
         raise ValidationError(ungettext(
             'Username must be at least one character long.',
@@ -31,6 +29,7 @@ def validate_username(value, db_settings):
         ) % {
             'count': db_settings['username_length_min'],
         })
+
     if len(value) > db_settings['username_length_max']:
         raise ValidationError(ungettext(
             'Username cannot be longer than one characters.',
@@ -39,18 +38,21 @@ def validate_username(value, db_settings):
         ) % {
             'count': db_settings['username_length_max'],
         })
+
     if settings.UNICODE_USERNAMES:
         if not re.search('^[^\W_]+$', value, re.UNICODE):
             raise ValidationError(_("Username can only contain letters and digits."))
     else:
         if not re.search('^[^\W_]+$', value):
             raise ValidationError(_("Username can only contain latin alphabet letters and digits."))
+
     if Ban.objects.check_ban(username=value):
         raise ValidationError(_("This username is forbidden."))
 
 
 def validate_password(value, db_settings):
     value = unicode(value).strip()
+
     if len(value) < db_settings['password_length']:
         raise ValidationError(ungettext(
             'Correct password has to be at least one character long.',
@@ -59,6 +61,7 @@ def validate_password(value, db_settings):
         ) % {
             'count': db_settings['password_length'],
         })
+
     for test in db_settings['password_complexity']:
         if test in ('case', 'digits', 'special'):
             if not re.search('[a-zA-Z]', value):