""" Human verification system for the comments plugin. Based on a idea and ref impl of Jesus Roncero Franco . Implemented as a pyblosxom plugin by Steven Armstrong . Creates a random number, generates an image of it, and stores the number in the session. Then compares the number the user entered in the comment form with the one stored in the session. Rejects the comment if they don't match. If you make any changes to this plugin, please send a patch to so I can incorporate them. Thanks! Thanks to Ludger Humbert for pointing out issues with different PIL versions. Thanks to Lance Levsen for adding the allow_trackback bypass and fixing some bugs. To install: 1) Put nospam.py in your plugin directory. 2) In config.py add nospam to py['load_plugins'] 3) Add the following variables to config.py: py['nospam_font'] = '/path/to/truetype/font.ttf' # required, no default py['nospam_extension'] = '/nospam.png' # optional, this is the default py['nospam_allow_trackback'] = 1 # optional to allow trackbacks to pass # through when using the trackback # plugin from Ted Leung Note: If you get an error about problems with TrueType fonts, it's likely that you are using an older PIL version that can't handle them. In this case you'll have to use a pil font from [1] instead and configure config.py as: py['nospam_font'] = '/path/to/pilfonts/lubB10.pil' [1] http://effbot.org/pil/pilfonts.zip Add something like this to your comment-form.html template: Secret Number Image Dependecies: - My compatibility plugin if you're not using pyblosxom 1.2+. - My session plugin. - Python imaging library from http://www.pythonware.com/products/pil/ $Id: nospam.py,v 1.6 2006/04/14 18:12:17 sar Exp $ """ __author__ = "Steven Armstrong " __version__ = "$Revision: 1.6 $ $Date: 2006/04/14 18:12:17 $" __url__ = "http://www.c-area.ch/code/" __description__ = "Human verification system for the comments plugin" __license__ = "GPL 2+" # Python imports import sys import os import random from Pyblosxom import tools # PIL imports http://www.pythonware.com/products/pil/ try: # use try/except so verify_installation can guide the user how to setup PIL import Image import ImageDraw import ImageOps try: import ImageFont except ImportError: import PIL.ImageFont as ImageFont except ImportError: pass # parameters _xstep = 5 _ystep = 5 _imageSize = (61,21) _bgColor = (255,255,255) # White _gridInk = (200,200,200) _fontInk = (130,130,130) _fontSize = 14 # set in cb_start callback _fontPath = None # the names of the fields used in the comment form _form_fields = ["title", "author", "email", "url", "body"] def verify_installation(request): config = request.getConfiguration() retval = 1 from Pyblosxom import pyblosxom version = pyblosxom.VERSION if version < 1.2: try: import compatibility except ImportError: print "You're running Pyblosxom %.1f and will need " % version print "the 'compatibility.py' plugin to use this plugin." retval = 0 try: import session except ImportError: print "Missing required plugin 'session.py'." retval = 0 old_pil = False no_pil = False try: import ImageFont except ImportError: old_pil = True try: import PIL.ImageFont except ImportError: no_pil = True if not config.has_key('nospam_font'): print "Missing required property: 'nospam_font'" print "This must be the absolute path to a truetype font." retval = 0 if no_pil: print "Python imaging library not found." print "Get and install it from http://www.pythonware.com/products/pil/" retval = 0 if old_pil: print "You seem to be using an old PIL (Python imaging library) version." print "You must get a pil font from http://effbot.org/pil/pilfonts.zip" print "and point the 'nospam_font' config property to one of them." if not config.has_key('nospam_extension'): print "Missing optional property: 'nospam_extension'" print "Using the default of '/nospam.png'" if not config.has_key('nospam_allow_trackback'): print "Missing optional property: 'nospam_allow_trackback'" print "Using the default of 0" return retval # This function is (c) Jesus Roncero Franco . # Modified to support old and new PIL versions by Steven Armstrong. def _generateImage(number): try: # recent PIL version with support for truetype fonts font = ImageFont.truetype(_fontPath, _fontSize) except AttributeError: # old PIL version, fallback to pil fonts font = ImageFont.load(_fontPath) img = Image.new("RGB", _imageSize, _bgColor) draw = ImageDraw.Draw(img) xsize, ysize = img.size # Do we want the grid start at 0,0 or want some offset? x, y = 0,0 while x <= xsize: try: # recent PIL version draw.line(((x, 0), (x, ysize)), fill=_gridInk) except TypeError: # old PIL version draw.setink(_gridInk) draw.line(((x, 0), (x, ysize))) x = x + _xstep while y <= ysize: try: draw.line(((0, y), (xsize, y)), fill=_gridInk) except TypeError: draw.setink(_gridInk) draw.line(((0, y), (xsize, y))) y = y + _ystep try: draw.text((10, 2), number, font=font, fill=_fontInk) except TypeError: draw.setink(_fontInk) draw.text((10, 2), number, font=font) return img def _writeImage(request): number = str(random.randrange(1,99999,1)) session = request.getSession() session["nospam"] = number session.save() image = _generateImage(number) response = request.getResponse() response.addHeader('Content-Type', 'image/png') image.save(response, "PNG") def _remember_comment(request): """ Stores form fields in the data dict so they can be used to refill the form in the template. @param request: pyblosxom request object @type request: L{Pyblosxom.pyblosxom.Request} """ data = request.getData() form = request.getForm() for key in _form_fields: data["cmt_%s" % key] = (form.has_key(key) and [form[key].value] or [''])[0] def _forget_comment(request): """ Resets/forgets any stored form field values. @param request: pyblosxom request object @type request: L{Pyblosxom.pyblosxom.Request} """ data = request.getData() for key in _form_fields: key = "cmt_%s" % key if key in data: del data[key] #****************************** # Callbacks #****************************** def cb_start(args): request = args['request'] config = request.getConfiguration() global _fontPath _fontPath = config.get('nospam_font') def cb_handle(args): request = args['request'] http = request.getHttp() config = request.getConfiguration() ext = config.get("nospam_extension", "/nospam.png") if http['PATH_INFO'].endswith( ext ): # write the image to the output stream _writeImage(request) # return True to tell pyblosxom that the request has been taken care of return 1 def cb_comment_reject(args): """ Checks if the the nospam number of the incomming request matches the one stored in the session. Creates a template variable $cmt_nospam_error with a error message if it didn't. Also creates the following template variables: $cmt_title, $cmt_author, $cmt_email, $cmt_url, $cmt_body which can be used to populate the form with the values provided by the user. If the config setting nospam_allow_trackback is True, and the comment came in via the trackback plugin the comment is accepted. @param args: a dict containing: pyblosxom request, comment dict @type config: C{dict} @return: True if the comment should be rejected, False otherwise @rtype: C{bool} """ request = args['request'] session = request.getSession() form = request.getForm() data = request.getData() config = request.getConfiguration() allow_trackback = config.get('nospam_allow_trackback', 0) try: nospam = int(form["nospam"].value) sess_nospam = int(session["nospam"]) except: nospam = 0 sess_nospam = 1 if allow_trackback: comment = args['comment'] #log = tools.getLogger() #log.info(comment['author']) if comment['author'].count("Trackback") > 0: return False if nospam != sess_nospam: _remember_comment(request) data["cmt_nospam_error"] = "Secret number did not match." return True else: _forget_comment(request) if "cmt_nospam_error" in data: del data["cmt_nospam_error"] return False