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 import __version__
+from misago.admin import site
 
 
-def misago(request):
+def common(request):
     try:
     try:
         return {
         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,
             'monitor': request.monitor,
+            'request_path': request.get_full_path(),
             'settings': request.settings,
             'settings': request.settings,
             'stopwatch': request.stopwatch.time(),
             'stopwatch': request.stopwatch.time(),
+            'user': request.user,
+            'version': __version__,
         }
         }
     except AttributeError:
     except AttributeError:
         # If request lacks required service, let template crash in context processor's stead
         # If request lacks required service, let template crash in context processor's stead
         return  {}
         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.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.is_team = (request.acl.team or request.user.is_god())
             request.user.save(force_update=True)
             request.user.save(force_update=True)
+            
         if request.session.team != request.user.is_team:
         if request.session.team != request.user.is_team:
             request.session.team = request.user.is_team
             request.session.team = request.user.is_team
             request.session.save()
             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.threadreadmodel import ThreadRead
 from misago.models.tokenmodel import Token
 from misago.models.tokenmodel import Token
 from misago.models.usermodel import User, Guest, Crawler
 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')
     statistics_name = _('Users Registrations')
 
 
+    class Meta:
+        app_label = 'misago'
+
     def is_god(self):
     def is_god(self):
         try:
         try:
             return self.is_god_cache
             return self.is_god_cache

+ 0 - 1
misago/models/watchedthreadmodel.py

@@ -1,5 +1,4 @@
 from django.db import models
 from django.db import models
-from misago.forums.signals import 
 from misago.signals import merge_thread, move_forum_content, move_thread
 from misago.signals import merge_thread, move_forum_content, move_thread
 
 
 class WatchedThread(models.Model):
 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.static',
     'django.core.context_processors.tz',
     'django.core.context_processors.tz',
     'django.contrib.messages.context_processors.messages',
     '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
 # Jinja2 Template Extensions
@@ -100,16 +92,16 @@ MIDDLEWARE_CLASSES = (
     'misago.middleware.cookiejar.CookieJarMiddleware',
     'misago.middleware.cookiejar.CookieJarMiddleware',
     'misago.middleware.settings.SettingsMiddleware',
     'misago.middleware.settings.SettingsMiddleware',
     'misago.middleware.monitor.MonitorMiddleware',
     '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.middleware.session.SessionMiddleware',
-    #'misago.bruteforce.middleware.JamMiddleware',
-    #'misago.csrf.middleware.CSRFMiddleware',
+    'misago.middleware.bruteforce.JamMiddleware',
+    'misago.middleware.csrf.CSRFMiddleware',
     'misago.middleware.banning.BanningMiddleware',
     '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',
     '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):
 def error404(request, message=None):
     return error_view(request, 404, message)
     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.core.exceptions import ValidationError
 from django.utils.translation import ungettext, ugettext_lazy as _
 from django.utils.translation import ungettext, ugettext_lazy as _
 from misago.models import Ban
 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
 from misago.utils.strings import slugify
 
 
 class validate_sluggable(object):
 class validate_sluggable(object):
@@ -23,6 +20,7 @@ class validate_sluggable(object):
 
 
 def validate_username(value, db_settings):
 def validate_username(value, db_settings):
     value = unicode(value).strip()
     value = unicode(value).strip()
+
     if len(value) < db_settings['username_length_min']:
     if len(value) < db_settings['username_length_min']:
         raise ValidationError(ungettext(
         raise ValidationError(ungettext(
             'Username must be at least one character long.',
             'Username must be at least one character long.',
@@ -31,6 +29,7 @@ def validate_username(value, db_settings):
         ) % {
         ) % {
             'count': db_settings['username_length_min'],
             'count': db_settings['username_length_min'],
         })
         })
+
     if len(value) > db_settings['username_length_max']:
     if len(value) > db_settings['username_length_max']:
         raise ValidationError(ungettext(
         raise ValidationError(ungettext(
             'Username cannot be longer than one characters.',
             'Username cannot be longer than one characters.',
@@ -39,18 +38,21 @@ def validate_username(value, db_settings):
         ) % {
         ) % {
             'count': db_settings['username_length_max'],
             'count': db_settings['username_length_max'],
         })
         })
+
     if settings.UNICODE_USERNAMES:
     if settings.UNICODE_USERNAMES:
         if not re.search('^[^\W_]+$', value, re.UNICODE):
         if not re.search('^[^\W_]+$', value, re.UNICODE):
             raise ValidationError(_("Username can only contain letters and digits."))
             raise ValidationError(_("Username can only contain letters and digits."))
     else:
     else:
         if not re.search('^[^\W_]+$', value):
         if not re.search('^[^\W_]+$', value):
             raise ValidationError(_("Username can only contain latin alphabet letters and digits."))
             raise ValidationError(_("Username can only contain latin alphabet letters and digits."))
+
     if Ban.objects.check_ban(username=value):
     if Ban.objects.check_ban(username=value):
         raise ValidationError(_("This username is forbidden."))
         raise ValidationError(_("This username is forbidden."))
 
 
 
 
 def validate_password(value, db_settings):
 def validate_password(value, db_settings):
     value = unicode(value).strip()
     value = unicode(value).strip()
+
     if len(value) < db_settings['password_length']:
     if len(value) < db_settings['password_length']:
         raise ValidationError(ungettext(
         raise ValidationError(ungettext(
             'Correct password has to be at least one character long.',
             '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'],
             'count': db_settings['password_length'],
         })
         })
+
     for test in db_settings['password_complexity']:
     for test in db_settings['password_complexity']:
         if test in ('case', 'digits', 'special'):
         if test in ('case', 'digits', 'special'):
             if not re.search('[a-zA-Z]', value):
             if not re.search('[a-zA-Z]', value):