Integrating Captcha Service into Turbine

If you want to enable anonymous data input in your Turbine application like user registration and etc. you need to protect yourself from spammer robots. There are a lot of captcha (Completely Automated Public Test to tell Computers and Humans Apart) modul, I prefer JCaptcha from http://jcaptcha.sourceforge.net/. It has a lot of parameters like:

  • character set for generation
  • min and max length of text
  • image width and height in pixels
  • font min and max size
  • font color

All of these parameters can be setting in TR.properties:

services.CaptchaService.classname=com.zamek.portal_zamek.services.captcha.TurbineCaptchaService
services.CaptchaService.earlyInit=true
services.CaptchaService.chars=ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789   #character set
services.CaptchaService.parser.min=5   #minimum length of captcha text
services.CaptchaService.parser.max=6  #maximun length of captcha text
services.CaptchaService.size.width=200  #width of captcha image (px)
services.CaptchaService.size.height=100 #height of captcha image (px)
services.CaptchaService.font.min=25  #minimum size of captcha text font (px)
services.CaptchaService.font.max=30 #maximum size of captcha text font (px)
services.CaptchaService.fontcolor=blue #color of captcha text 

Using it on Velocity forms

If you need to create an anonymous form, you can call a macro like this:

#macro (formCaptchaField)
    #if (! $data.User.hasLoggedIn())
        <tr>
            <td colspan="2">
                <div align="center">
                <img src="$link.setAction('CaptchaAction').toString()" border="0">
                </div>
            </td>
        </tr>
        <tr>
            <td>
                Please enter the preceeding text
            </td>
            <td width="$inputWidth" class="frminput">
                <input type="text" maxlength="10" name="captcha_response" value="">
            </td>
        </tr>
    #end
#end

if user has not logged in, captcha is inserting into the form. And we need an action named 'C'aptcha'A'ction to serve captcha challenge images to form. It is a descendant of 'V'elocity'A'ction:

package com.zamek.portal_zamek.modules.actions;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Locale;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;

import org.apache.turbine.util.RunData;
import org.apache.velocity.context.Context;

import com.zamek.portal_zamek.ZamekConst;
import com.zamek.portal_zamek.services.captcha.TurbineCaptcha;

public class CaptchaAction extends VelocityAction
{
   private final static String LOGHEADER = "com.zamek.portal_zamek.modules.actions.";

   public void doPerform(RunData data, Context ctx) throws Exception
   {
       data.declareDirectResponse();  // declaring directresponse layout
       data.setLayout("DirectResponseLayout");
       processRequest(data);
   }

   private void processRequest(RunData data) throws ServletException, IOException
   {
        HttpServletResponse response = data.getResponse();
        response.setContentType("image/jpeg");
        try
       {
            String captchaId = data.getSession().getId();
            Locale locale = data.getLocale();
            ByteArrayOutputStream jpegOutputStream =
                   TurbineCaptcha.getImage(captchaId, locale);
            response.setContentLength(jpegOutputStream.size());
            jpegOutputStream.writeTo(response.getOutputStream());
        }
        catch (Exception e)
        {
            log.error(LOGHEADER+" error:"+e.getMessage());
        }
   }
}

Checking response

Service stores session id and original text, and if you get back the form data you can check it like this:

    public void doInsert (RunData data, Context context) {
        try
        {
            if (TurbineCaptcha.checkResponse(data)) {
                 codes to match response
            }
            else
                 setTemplate(data, FRM_REG);  // response doesn' t match
        }
        catch (CaptchaServiceException ce)
        {
            setTemplate(data, FRM_REG);
            data.addMessage(Localization.getString(ZamekConst.BUNDLE_CAPTCHA_ERROR));
        }
    }

The Service package

Finally we need to make captcha service package. There are 5 java class in package:

  • 'C'aptcha'S'ervice.java interface with constant declarations
  • 'T'urbine'C'aptcha'S'ervice.java the core service code
  • 'T'urbine'C'aptcha.java static adapter to service
  • 'C'aptcha'G'enerator.java extends 'D'efault'M'anageable'I'mage'C'aptcha'S'ervice
  • 'C'aptcha'E'ngine.java extends 'L'ist'I'mage'C'aptcha'E'ngine implements parameter settings

CaptchaService.java

package com.zamek.portal_zamek.services.captcha;

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Locale;

import org.apache.turbine.services.Service;
import org.apache.turbine.util.RunData;

public interface CaptchaService extends Service {

    /**
     * The service identifier
     */
    public final static String SERVICE_NAME = "CaptchaService";

    /**
     * chars key in config
     */
    public final static String CHARS_KEY = "chars";

    /**
     * default chars
     */
    public final static String DEFAULT_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    /**
     * min text length key in config
     */
    public final static String PARSER_MIN_KEY = "parser.min";

    /**
     * default min length
     */
    public final static int DEFAULT_PARSER_MIN = 5;

    /**
     * max text length key in config
     */
    public final static String PARSER_MAX_KEY = "parser.max";

    /**
     * default max length
     */
     public final static int DEFAULT_PARSER_MAX = 8;

    /**
     * size width key in config
     */
    public final static String SIZE_WIDTH_KEY = "size.width";
    
    /**
     * default size width
     */
    public final static int DEFAULT_SIZE_WIDTH = 200;

    /**
     * size height key in config
     */
    public final static String SIZE_HEIGHT_KEY = "size.height";
    
    /**
     * default size height
     */
    public final static int DEFAULT_SIZE_HEIGHT = 100;
        
    /**
     * min font size key in config
     */
    public final static String MIN_FONT_KEY = "font.min";
    
    /**
     * default font min
     */
    public final static int DEFAULT_FONT_MIN = 25;
    
    /**
     * max font size key in config
     */
    public final static String MAX_FONT_KEY = "font.max";
    
    /**
     * default font max
     */
    public final static int DEFAULT_FONT_MAX = 30;

    /**
     * font color key in config
     */
    public final static String FONT_COLOR_KEY = "fontcolor";
    
    /**
     * default font color
     */
    public final static String DEFAULT_FONT_COLOR = "white";
    
    /**
     * name of captcha field on form
     */
    public final static String CTX_CAPTCHA = "captcha";
    
    /**
     * name of captcha response field on form
     */
    public final static String CTX_CAPTCHA_RESPONSE = "captcha_response";
    
    /**
     * Captcha error message form localized messages
     */
    public final static String BUNDLE_CAPTCHA_ERROR = "captchaError";
    
    
    /**
     * getting a new captcha
     * @param captchaId session id
     * @param locale locales for request
     * @return an image
     */
    public BufferedImage getCaptcha(final String captchaId, final Locale locale);

    /**
     * getting a captcha image
     * @param captchaId session id
     * @param locale locales for request
     * @return an jpeg stream
     */
    public ByteArrayOutputStream getImage(final String captchaId, final Locale locale) throws IOException;
    
    
    /**
     * check response
     * @param data
     * @return
     */
    public boolean checkResponse (RunData data);

    /**
     * validating response for session id
     * @param id session id
     * @param response text of response
     * @return
     */
    public boolean validateResponseForID(final String id, final String response);

}

TurbineCaptchaService.java

package com.zamek.portal_zamek.services.captcha;

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Locale;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.turbine.services.InitializationException;
import org.apache.turbine.services.TurbineBaseService;
import org.apache.turbine.services.localization.Localization;
import org.apache.turbine.util.RunData;

import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGImageEncoder;

public class TurbineCaptchaService extends TurbineBaseService 
    implements    CaptchaService {

    protected Log log = LogFactory.getLog(TurbineCaptchaService.class);
    private final static String LOGHEADER="com.zamek.portal_zamek.services.captcha.";

   private CaptchaGenerator generator = null;

    /**
     * initialize service
     */
    public void init() throws InitializationException {
        generator = new CaptchaGenerator();
        setInit(true);
        log.debug(LOGHEADER+"init ok");
    }

    /**
     * getting a new captcha
     * @param captchaId session id
     * @param locale locales for request
     * @return an image
     */
    public BufferedImage getCaptcha(final String captchaId, final Locale locale)
    {
        if (log.isDebugEnabled())
            log.debug(String.format(
                    LOGHEADER + "getCaptcha enter, captchaId:%s, locale:%s",
                    captchaId, locale));
        try {
            BufferedImage challenge =
                  generator.getImageChallengeForID(captchaId, locale);
            return challenge;
        }
        catch (Exception e) {
          log.error(LOGHEADER + e.getMessage());
          return null;
        }
    }

    /**
     * check response
     * @param data
     * @return
     */
    public boolean checkResponse(RunData data)
    {
        String id = data.getSession().getId(),
           response = data.getParameters().get(CaptchaService.CTX_CAPTCHA_RESPONSE);
        if (StringUtils.isNotEmpty(id) && StringUtils.isNotEmpty(response) &&
                validateResponseForID(id, response))
            return true;
        data.addMessage(Localization.getString(CaptchaService.BUNDLE_CAPTCHA_ERROR));
        return false;
    }
    
    /**
     * validating response for session id
     * @param id session id
     * @param response text of response
     * @return
     */
    public boolean validateResponseForID(final String id, final String response)
    {
        return generator.validateResponseForID(id, response);
    }
    
    /**
     *
     * @param captchaId session id
     * @param locale locales for request
     * @return an jpeg stream
     */
    public ByteArrayOutputStream getImage(final String captchaId, final Locale locale)
        throws IOException
    {
        ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
        BufferedImage challenge = this.getCaptcha(captchaId, locale);
        // a jpeg encoder
        JPEGImageEncoder jpegEncoder =
              JPEGCodec.createJPEGEncoder(jpegOutputStream);
        jpegEncoder.encode(challenge);
        return jpegOutputStream;
    
    }
}

TurbineCaptcha.java

package com.zamek.portal_zamek.services.captcha;

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Locale;

import org.apache.turbine.services.TurbineServices;
import org.apache.turbine.util.RunData;

public class TurbineCaptcha {

    /**
     * get service object
     * @return
     */
    public static CaptchaService getService()
    {
        return (CaptchaService) TurbineServices.getInstance().
                getService(CaptchaService.SERVICE_NAME);
    }

    /**
     * checking service available
     * @return
     * @throws InstantiationException
     */
    public static boolean isAvailable() throws InstantiationException
    {
        try
        {
            @SuppressWarnings("unused")
            CaptchaService captcha = getService();
            return true;
        }
        catch (Exception ie) {
            return false;
        }
    }
    
    /**
     * getting a captcha challenge image
     * @param captchaId
     * @param locale
     * @return
     */
    public static BufferedImage getCaptcha(final String captchaId, final Locale locale)
    {
        return getService().getCaptcha(captchaId, locale);
    }

    /**
     * checking response of user
     * @param data
     * @return
     */
    public static boolean checkResponse(RunData data)
    {
        return getService().checkResponse(data);
    }
    
    /**
     * validate user's response to compare stored text
     * @param id
     * @param response
     * @return
     */
    public static boolean validateResponseForID(final String id, final String response)
    {
        return getService().validateResponseForID(id, response);
    }
    
    /**
     * getting a challenge image
     * @param captchaId
     * @param locale
     * @return
     * @throws IOException
     */
    public static ByteArrayOutputStream getImage(final String captchaId, final Locale locale)
        throws IOException
    {
        return getService().getImage(captchaId, locale);
    }

}

CaptchaGenerator.java

package com.zamek.portal_zamek.services.captcha;

import com.octo.captcha.service.image.DefaultManageableImageCaptchaService;

public class CaptchaGenerator extends DefaultManageableImageCaptchaService {

    public CaptchaGenerator() {
        super();
        this.setCaptchaEngine(new CaptchaEngine());
    }
}

CaptchaEngine.java

package com.zamek.portal_zamek.services.captcha;

import java.awt.Color;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.turbine.Turbine;
import com.octo.captcha.component.image.backgroundgenerator.BackgroundGenerator;
import com.octo.captcha.component.image.backgroundgenerator.FunkyBackgroundGenerator;
import com.octo.captcha.component.image.fontgenerator.FontGenerator;
import com.octo.captcha.component.image.fontgenerator.TwistedAndShearedRandomFontGenerator;
import com.octo.captcha.component.image.textpaster.RandomTextPaster;
import com.octo.captcha.component.image.textpaster.TextPaster;
import com.octo.captcha.component.image.wordtoimage.ComposedWordToImage;
import com.octo.captcha.component.image.wordtoimage.WordToImage;
import com.octo.captcha.component.word.wordgenerator.RandomWordGenerator;
import com.octo.captcha.component.word.wordgenerator.WordGenerator;
import com.octo.captcha.engine.image.ListImageCaptchaEngine;
import com.octo.captcha.image.gimpy.GimpyFactory;

public class CaptchaEngine extends ListImageCaptchaEngine
{
    private final static String LOGHEADER = "com.zamek.portal_zamek.services.captcha.";
    private final static Log log = LogFactory.getLog(CaptchaEngine.class);
    
    @Override
    protected void buildInitialFactories()
    {
        Configuration conf = Turbine.getConfiguration();
        int parserMin = conf.getInt(
                    CaptchaService.PARSER_MIN_KEY, CaptchaService.DEFAULT_PARSER_MIN),
            parserMax = conf.getInt(
                    CaptchaService.PARSER_MAX_KEY, CaptchaService.DEFAULT_PARSER_MAX),
            sizeWidth = conf.getInt(
                    CaptchaService.SIZE_WIDTH_KEY, CaptchaService.DEFAULT_SIZE_WIDTH),
            sizeHeight = conf.getInt(
                    CaptchaService.SIZE_HEIGHT_KEY, CaptchaService.DEFAULT_SIZE_HEIGHT),
            fontMin = conf.getInt(
                    CaptchaService.MIN_FONT_KEY, CaptchaService.DEFAULT_FONT_MIN),
            fontMax = conf.getInt(
                    CaptchaService.MAX_FONT_KEY, CaptchaService.DEFAULT_FONT_MAX);
            String chars = conf.getString(
                    CaptchaService.CHARS_KEY, CaptchaService.DEFAULT_CHARS),
                   colorName = conf.getString(
                    CaptchaService.FONT_COLOR_KEY, CaptchaService.DEFAULT_FONT_COLOR);
            Color fontColor = Color.getColor(colorName);
            if (log.isDebugEnabled())
                log.debug(LOGHEADER +
                        String.format("init, parserMin:%d, parserMax:%d, sizeWidth:%d, sizeHeight:%d, fontMin:%d, fontMax:%d, chars:%s",
                                      parserMin, parserMax, sizeWidth, sizeHeight, fontMin, fontMax, chars));
            WordGenerator wordGenerator = new RandomWordGenerator(chars);
    
            TextPaster textParser = new RandomTextPaster(parserMin, parserMax, fontColor);
    
            BackgroundGenerator backgroundGenerator =
                new FunkyBackgroundGenerator (sizeWidth, sizeHeight);
    
            FontGenerator fontGenerator =
                new TwistedAndShearedRandomFontGenerator (fontMin, fontMax);
    
            WordToImage wordToImage = new ComposedWordToImage(fontGenerator, backgroundGenerator, textParser);
            this.addFactory(new GimpyFactory(wordGenerator, wordToImage));
    }
}
  • No labels