Commits

Lynn Rees committed a8bb2d6

[svn]

  • Participants
  • Parent commits 364297f
  • Branches wsgiakismet

Comments (0)

Files changed (3)

File trunk/test_akismet.py

+#!/usr/bin/python
+# Version 0.1.2
+# 2005/12/05
+
+# Copyright Michael Foord 2005
+# Copyright L.C. Rees
+# test_akismet.py
+# Test CGI for akismet.py - the Python interface to the akismet API
+
+# http://www.voidspace.org.uk/python/modules.shtml
+# http://akismet.com
+
+# Released subject to the BSD License
+# Please see http://www.voidspace.org.uk/python/license.shtml
+
+# For information about bugfixes, updates and support, please join the Pythonutils mailing list.
+# http://groups.google.com/group/pythonutils/
+# Comments, suggestions and bug reports welcome.
+# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml
+# E-mail fuzzyman@voidspace.org.uk
+
+import unittest
+import wsgiakismet
+from StringIO import StringIO 
+
+sub = 'comment_author=%s&comment_author_email%s&comment_author_url=%s&comment=%s&submit=Submit'
+name = 'viagra-test-123'
+key = '707659935d35'
+blog = 'http://www.foppoff.com/'
+
+
+class TestWsgiAkisimet(unittest.TestCase):
+
+    def dummy_sr(self, status, headers, exc_info=None):
+        pass
+
+    def test_negative(self):
+        env = {
+        'wsgi.input':StringIO(sub % ('Bob Saunders', 'bob@saunders.net', '', 'Nice post')),
+        'REMOTE_ADDR':'207.89.134.5',
+        'HTTP_USER_AGENT':'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; YPC 3.0.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)'
+        }
+        @wsgiakismet.akismet(key, blog)
+        def test(environ, start_response):
+            start_response('200 OK', [('Content-type', 'text/plain')])
+            return ['Good']
+        response = test(env, self.dummy_sr)
+        self.assertEqual(response[0], 'Good')
+
+    def test_positive(self):
+        env = {
+        'wsgi.input':StringIO(sub % (name, 'bob@saunders.net', '', 'Nice post')),
+        'REMOTE_ADDR':'10.9.4.59',
+        'HTTP_USER_AGENT':'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; YPC 3.0.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)'
+        }        
+        @wsgiakismet.akismet(key, blog)
+        def test(environ, start_response):
+            start_response('200 OK', [('Content-type', 'text/plain')])
+            return ['Good']
+        response = test(env, self.dummy_sr)
+        self.assertEqual(response[0], 'Comment was spam.')  
+        
+
+
+if __name__ == '__main__': unittest.main()
+       

File trunk/tests/test_wsgiakismet.py

+#!/usr/bin/python
+# Version 0.1.2
+# 2005/12/05
+
+# Copyright Michael Foord 2005
+# Copyright L.C. Rees
+# test_akismet.py
+# Test CGI for akismet.py - the Python interface to the akismet API
+
+# http://www.voidspace.org.uk/python/modules.shtml
+# http://akismet.com
+
+# Released subject to the BSD License
+# Please see http://www.voidspace.org.uk/python/license.shtml
+
+# For information about bugfixes, updates and support, please join the Pythonutils mailing list.
+# http://groups.google.com/group/pythonutils/
+# Comments, suggestions and bug reports welcome.
+# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml
+# E-mail fuzzyman@voidspace.org.uk
+
+import unittest
+import wsgiakismet
+from StringIO import StringIO 
+from urllib import urlencode
+
+key = '707659935d35'
+blog = 'http://www.foppoff.com/'
+
+
+class TestWsgiAkisimet(unittest.TestCase):
+
+    def dummy_sr(self, status, headers, exc_info=None):
+        pass
+
+    def test_negative(self):
+        env = {
+        'HTTP_METHOD':'POST',
+        'wsgi.input':StringIO(''),
+        'QUERY_STRING':urlencode({'comment_author':'Bob Saunders',
+            'comment_author_email':'bob@saunders.net', 'comment_author_url':'',
+            'comment':'The post was informative.'}),
+        'REMOTE_ADDR':'207.89.134.5',
+        'HTTP_USER_AGENT':'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; YPC 3.0.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)'
+        }
+        @wsgiakismet.akismet(key, blog)
+        def test(environ, start_response):
+            start_response('200 OK', [('Content-type', 'text/plain')])
+            return ['Good']
+        response = test(env, self.dummy_sr)
+        self.assertEqual(response[0], 'Good')
+
+    def test_positive(self):
+        env = {
+        'HTTP_METHOD':'POST',
+        'wsgi.input':StringIO(''),
+        'QUERY_STRING':urlencode({'comment_author':'viagra-test-123',
+            'comment_author_email':'viagra@viagraoffer.net', 'comment_author_url':'',
+            'comment':'VIAGRA! LOTS OF VIAGRA!'}),
+        'REMOTE_ADDR':'10.9.4.59',
+        'HTTP_USER_AGENT':'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; YPC 3.0.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)'
+        }
+        @wsgiakismet.akismet(key, blog)
+        def test(environ, start_response):
+            start_response('200 OK', [('Content-type', 'text/plain')])
+            return ['Good']
+        response = test(env, self.dummy_sr)
+        self.assertEqual(response[0], 'Comment was spam.')  
+        
+
+
+if __name__ == '__main__': unittest.main()
+       

File trunk/wsgiakismet.py

-# Based on akisimetVersion 0.1.3
-# 2006/07/18
-
+# Based on akisimetVersion 0.1.3
+# 2006/07/18
+
 # Copyright Michael Foord 2005 & 2006
-# Copyright L.C. Rees 2006
-# akismet.py
-# Python interface to the akismet API
-
-# http://www.voidspace.org.uk/python/modules.shtml
-# http://akismet.com
-
-# Released subject to the BSD License
-# Please see http://www.voidspace.org.uk/python/license.shtml
-
-# For information about bugfixes, updates and support, please join the Pythonutils mailing list.
-# http://groups.google.com/group/pythonutils/
-# Comments, suggestions and bug reports welcome.
-# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml
-# E-mail fuzzyman@voidspace.org.uk
-
-'''A python interface to the `Akismet <http://akismet.com>`_ 
-{acro;API;Application Programmers Interface}. This is a web service for
-blocking SPAM comments to blogs - or other online services.
-
-You will need a Wordpress API key, from `wordpress.com <http://wordpress.com>`_.
-
-You should pass in the keyword argument 'agent' to the name of your program,
-when you create an Akismet instance. This sets the ``user-agent`` to a useful
-value.
-
-The default is : ::
-
-    Python Interface by Fuzzyman | akismet.py/0.1.3
-
-Whatever you pass in, will replace the *Python Interface by Fuzzyman* part.
-**0.1.2** will change with the version of this interface.
+# Copyright L.C. Rees 2006
+# akismet.py
+# Python interface to the akismet API
+
+# http://www.voidspace.org.uk/python/modules.shtml
+# http://akismet.com
+
+# Released subject to the BSD License
+# Please see http://www.voidspace.org.uk/python/license.shtml
+
+'''WSGI middleware implementing the `Akismet <http://akismet.com>`_ 
+{acro;API;Application Programmers Interface}. This is a web service for
+blocking SPAM comments to blogs - or other online services.
+
+You will need a Wordpress API key, from `wordpress.com <http://wordpress.com>`_.
+
+You should pass in the keyword argument 'agent' to the name of your program,
+when you create an Akismet instance. This sets the ``user-agent`` to a useful
+value. The default is : ::
+
+    Python Interface by Fuzzyman | akismet.py/0.1.3
+
+Whatever you pass in, will replace the *Python Interface by Fuzzyman* part.
+**0.1.2** will change with the version of this interface.
 '''
-
+
 import cgi
 import urllib2
 import socket
-from urllib import urlencode
-from StringIO import StringIO
+from urllib import urlencode
+from StringIO import StringIO
 
-__all__ = ('__version__', 'Akismet', 'AkismetError', 'APIKeyError')
-__version__ = '0.1.3'
-__author__ = 'Michael Foord <fuzzyman AT voidspace DOT org DOT uk>'
+__all__ = ('__version__', 'Akismet', 'akismet')
+__version__ = '0.1.3'
+__author__ = 'Michael Foord <fuzzyman AT voidspace DOT org DOT uk>'
 __docformat__ = 'restructuredtext en'
-user_agent = '%s | wsgiakismet.py/%s'
-DEFAULTAGENT = 'Python Interface by Fuzzyman/%s'
+user_agent = '%s | wsgiakismet.py/%s'
+DEFAULTAGENT = 'Python Interface by Fuzzyman/%s'
 
-def _formparse(environ, strict=False):
+if hasattr(socket, 'setdefaulttimeout'):
+    # Set the default timeout on sockets to 5 seconds
+    socket.setdefaulttimeout(5)
+
+def formparse(env):
     '''Extracts data from form submissions.
 
     @param environ Environment dictionary
     @param strict Stops on errors (default: False)
     '''
-    winput = environ['wsgi.input']
+    winput = env['wsgi.input']
     if hasattr(winput, 'getvalue'):
         tinput = winput.getvalue()
     else:
         tinput = winput.read()
-        environ['wsgi.input'] = StringIO(tinput)
-    qdict = cgi.parse(StrinIO(tinput), environ, strict, strict)
+        env['wsgi.input'] = StringIO(tinput)
+    qdict = cgi.parse(fp=StringIO(tinput), environ=env)
     # Remove invididual entries from list and store as naked string
     for key, value in qdict.iteritems():
         if len(value) == 1: qdict[key] = value[0]
     def decorator(application):
         return WsgiAkismet(application, key, blog_url, **kw)
     return decorator
-
-
+
+
 class WsgiAkismet(object):
-    
-    '''WSGI middleware for working with the akismet API'''
-
+    
+    '''WSGI middleware for working with the akismet API'''
+
+    # Central Akismet server
     baseurl = 'rest.akismet.com/1.1/'
-    vals = set(['comment_type', 'comment_author', 'comment_author_email',
-            'comment_author_url', 'permalink'])
-
+    # Needed form values
+    vals = set(['comment_type', 'comment_author', 'comment_author_email',
+            'comment_author_url', 'permalink'])
+
     def __init__(self, application, key, blog_url, **kw):
         self.application = application
-        agent = kw.get('agent', DEFAULTAGENT % __version__)
+        self.key, self.blog_url = key, blog_url           
+        agent = kw.get('agent', DEFAULTAGENT % __version__)
         self.user_agent = user_agent % (agent, __version__)
+        # Verify key if assertions enabled     
+        self.verify = kw.get('verify', False)
+        # Form key for comment 
         self.comment_key = kw.get('comment', 'comment')
-        self.handler = kw.get('handler', _handler)
-        self.key, self.blog_url = key, blog_url
-        if hasattr(socket, 'setdefaulttimeout'):
-            # Set the default timeout on sockets to 5 seconds
-            socket.setdefaulttimeout(kw.get('timeout', 5))
+        # Stub handler -- change
+        self.handler = kw.get('handler', _handler)        
 
     def __call__(self, environ, start_response):
-        formdata = _formparse(environ)
+        # Parse form
+        formdata = formparse(environ)
+        # Get comment
         comment = formdata[self.comment_key]
+        # Fetch any corresponding values from the form submission
         data = self._getvalues(formdata)
-        if self.comment_check(environ, comment, data):
-            return self.application(environ, start_response)
-        else:
+        # Return handler if comment is spam
+        if self.iscomment(environ, comment, data):
             return self.handler(environ, start_response)
+        return self.application(environ, start_response)        
 
-    def _getvalues(self, data):        
-        return dict((k, data[k]) for k in data if k in self.vals)
-
-    def _build_data(self, environ, comment, data):
-        '''This function builds the data structure required by ``comment_check``,
-        ``submit_spam``, and ``submit_ham``.
+    def _getvalues(self, data):
+        '''Gets any form values corresponding to Akismet request.'''
+        return dict((k, data[k]) for k in data if k in self.vals)
+
+    def _build_data(self, environ, comment, data):
+        '''This function builds the data structure required by ``iscomment``.
+        
+        It modifies the ``data`` dictionary you give it in place. (and so
+        doesn't return anything)
+        '''
+        # Add comment
+        data['comment_content'] = comment
+        data['user_ip'] = environ.get('REMOTE_ADDR', '')
+        data['user_agent'] = environ.get('HTTP_USER_AGENT', '')
+        # Note the spelling 'referrer'. This is a required value by the
+        # akismet api - however, referrer information is not always
+        # supplied by the browser or server. In fact the HTTP protocol
+        # forbids relying on referrer information for functionality in 
+        # programs.
+        data['referrer'] = environ.get('HTTP_REFERER', 'unknown')
+        data.setdefault('permalink', '')
+        # The `API docs <http://akismet.com/development/api/>`_ state that this
+        # value can be '*blank, comment, trackback, pingback, or a made up value*
+        # *like 'registration'* '.
+        data.setdefault('comment_type', 'comment')
+        data.setdefault('comment_author', '')
+        data.setdefault('comment_author_email', '')
+        data.setdefault('comment_author_url', '')
+        data['SERVER_ADDR'] = environ.get('SERVER_ADDR', '')
+        data['SERVER_ADMIN'] = environ.get('SERVER_ADMIN', '')
+        data['SERVER_NAME'] = environ.get('SERVER_NAME', '')
+        data['SERVER_PORT'] = environ.get('SERVER_PORT', '')
+        data['SERVER_SIGNATURE'] = environ.get('SERVER_SIGNATURE', '')
+        data['SERVER_SOFTWARE'] = environ.get('SERVER_SOFTWARE', '')
+        data['HTTP_ACCEPT'] = environ.get('HTTP_ACCEPT', '')
+        data['blog'] = self.blog_url
+
+    def isvalidkey(self):
+        '''This equates to the ``verify-key`` call against the akismet API.
         
-        It modifies the ``data`` dictionary you give it in place. (and so
-        doesn't return anything)
+        It returns ``True`` if the key is valid.
         
-        It raises an ``AkismetError`` if the user IP or user-agent can't be
-        worked out.
-        '''
-        data['comment_content'] = comment
-        data['user_ip'] = environ['REMOTE_ADDR']
-        data['user_agent'] = environ['HTTP_USER_AGENT']
-        data['referrer'] = environ.get('HTTP_REFERER', 'unknown')
-        data.setdefault('permalink', '')
-        data.setdefault('comment_type', 'comment')
-        data.setdefault('comment_author', '')
-        data.setdefault('comment_author_email', '')
-        data.setdefault('comment_author_url', '')
-        data['SERVER_ADDR'] = environ.get('SERVER_ADDR', '')
-        data['SERVER_ADMIN'] = environ.get('SERVER_ADMIN', '')
-        data['SERVER_NAME'] = environ.get('SERVER_NAME', '')
-        data['SERVER_PORT'] = environ.get('SERVER_PORT', '')
-        data['SERVER_SIGNATURE'] = environ.get('SERVER_SIGNATURE', '')
-        data['SERVER_SOFTWARE'] = environ.get('SERVER_SOFTWARE', '')
-        data['HTTP_ACCEPT'] = environ.get('HTTP_ACCEPT', '')
-        data['blog'] = self.blog_url
-
-    def comment_check(self, environ, comment, data):
-        '''This is the function that checks comments.
-        
-        It returns ``True`` for spam and ``False`` for ham.
-        It raises ``APIKeyError`` if you have not yet set an API key.
-        
-        If the connection to Akismet fails then the ``HTTPError`` or
-        ``URLError`` will be propogated.
-        
-        As a minimum it requires the body of the comment. This is the
-        ``comment`` argument.
-        
-        Akismet requires some other arguments, and allows some optional ones.
-        The more information you give it, the more likely it is to be able to
-        make an accurate diagnosise.
-        
-        You supply these values using a mapping object (dictionary) as the
-        ``data`` argument.
-        
-        If ``build_data`` is ``True`` (the default), then *akismet.py* will
-        attempt to fill in as much information as possible, using default
-        values where necessary. This is particularly useful for programs
-        running in a {acro;CGI} environment. A lot of useful information
-        can be supplied from evironment variables (``os.environ``). See below.
-        
-        You *only* need supply values for which you don't want defaults filled
-        in for. All values must be strings.
-        
-        There are a few required values. If they are not supplied, and
-        defaults can't be worked out, then an ``AkismetError`` is raised.
-        
-        If you set ``build_data=False`` and a required value is missing an
-        ``AkismetError`` will also be raised.
-        
-        The normal values (and defaults) are as follows : ::
-        
-            'user_ip':          environ['REMOTE_ADDR']       (*)
-            'user_agent':       environ['HTTP_USER_AGENT']   (*)
-            'referrer':         environ.get('HTTP_REFERER', 'unknown') [#]_
-            'permalink':        ''
-            'comment_type':     'comment' [#]_
-            'comment_author':   ''
-            'comment_author_email': ''
-            'comment_author_url': ''
-            'SERVER_ADDR':      environ.get('SERVER_ADDR', '')
-            'SERVER_ADMIN':     environ.get('SERVER_ADMIN', '')
-            'SERVER_NAME':      environ.get('SERVER_NAME', '')
-            'SERVER_PORT':      environ.get('SERVER_PORT', '')
-            'SERVER_SIGNATURE': environ.get('SERVER_SIGNATURE', '')
-            'SERVER_SOFTWARE':  environ.get('SERVER_SOFTWARE', '')
-            'HTTP_ACCEPT':      environ.get('HTTP_ACCEPT', '')
-        
-        (*) Required values
-        
-        You may supply as many additional 'HTTP_*' type values as you wish.
-        These should correspond to the http headers sent with the request.
-        
-        .. [#] Note the spelling 'referrer'. This is a required value by the
-            akismet api - however, referrer information is not always
-            supplied by the browser or server. In fact the HTTP protocol
-            forbids relying on referrer information for functionality in 
-            programs.
-        .. [#] The `API docs <http://akismet.com/development/api/>`_ state that this value
-            can be ' *blank, comment, trackback, pingback, or a made up value*
-            *like 'registration'* '.
-        '''
-        self._build_data(environ, comment, data)
-        url = '%scomment-check' % ('http://%s.%s' % (self.key, self.baseurl))
-        # we *don't* trap the error here
-        # so if akismet is down it will raise an HTTPError or URLError
-        headers = {'User-Agent':self.user_agent}
+        The docs state that you *ought* to call this at the start of the
+        transaction.'''        
+        data = {'key':self.key, 'blog':self.blog_url}
+        # this function *doesn't* use the key as part of the URL
+        url = 'http://%sverify-key' % self.baseurl
+        headers = {'User-Agent' : self.user_agent}        
         req = urllib2.Request(url, urlencode(data), headers)
         try:
-            h = urllib2.urlopen(req)
-            resp = h.read().lower()
-            if resp == 'true': return True
+            if urllib2.urlopen(req).read().lower() == 'valid': return True
             return False
+        # Errors pass silently
         except:
-            return False
+            return False        
+
+    def iscomment(self, environ, comment, data):
+        '''This is the function that checks comments.
+        
+        It returns ``True`` for spam and ``False`` for ham.
+        
+        As a minimum it requires the environ and the body of the comment.
+        This is the ``comment`` argument.
+        
+        Akismet requires some other arguments, and allows some optional ones.
+        The more information you give it, the more likely it is to be able to
+        make an accurate diagnosise. You supply these values using a mapping
+        object (dictionary) as the ``data`` argument.
+        '''
+        # Verify API key if required
+        if self.verify and not self.isvalidkey(): return False
+        self._build_data(environ, comment, data)
+        url = '%scomment-check' % ('http://%s.%s' % (self.key, self.baseurl))
+        headers = {'User-Agent':self.user_agent}
+        req = urllib2.Request(url, urlencode(data), headers)
+        try:
+            if urllib2.urlopen(req).read().lower() == 'true': return True
+            return False
+        # Errors pass silently
+        except:
+            return True