001/*
002 * Shredzone Commons
003 *
004 * Copyright (C) 2012 Richard "Shred" Körber
005 *   http://commons.shredzone.org
006 *
007 * This program is free software: you can redistribute it and/or modify
008 * it under the terms of the GNU Library General Public License as
009 * published by the Free Software Foundation, either version 3 of the
010 * License, or (at your option) any later version.
011 *
012 * This program is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
015 * GNU General Public License for more details.
016 *
017 * You should have received a copy of the GNU Library General Public License
018 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
019 */
020package org.shredzone.commons.captcha.impl;
021
022import java.awt.image.BufferedImage;
023import java.util.Random;
024
025import javax.annotation.Resource;
026import javax.servlet.http.HttpSession;
027
028import org.shredzone.commons.captcha.CaptchaGenerator;
029import org.shredzone.commons.captcha.CaptchaService;
030import org.springframework.stereotype.Component;
031
032/**
033 * Default implementation of {@link CaptchaService}.
034 *
035 * @author Richard "Shred" Körber
036 */
037@Component("captchaService")
038public class DefaultCaptchaService implements CaptchaService {
039
040    private static final String CHARSET = "ABCDEFGHJLMNOPQRSTUWZ";
041    private static final int NUMBER_OF_CHARS = 5;
042    private static final String CAPTCHA_NAME = "captcha.position";
043    private static final String LASTCLICK_NAME = "captcha.lastclick";
044
045    private final Random rnd = new Random();
046
047    @Resource
048    private CaptchaGenerator captchaGenerator;
049
050    @Override
051    public BufferedImage createCaptcha(HttpSession session) {
052        int captchaPos = computeCaptchaPosition(session);
053        return captchaGenerator.createCaptcha(computeChars(captchaPos));
054    }
055
056    @Override
057    public boolean isValidCaptcha(HttpSession session, int x, int y) {
058        Integer pos = getCaptchaPosition(session);
059
060        if (pos == null) {
061            // There was no captcha generated yet, so the answer is always false.
062            return false;
063        }
064
065        int cw = captchaGenerator.getWidth();
066        int ch = captchaGenerator.getHeight();
067
068        if (x < 0 || y < 0 || x >= cw || y >= ch) {
069            // The click was outside of the captcha, so the answer is always false.
070            return false;
071        }
072
073        if (x == 0 && y == 0) {
074            // Ignore the simplest possible coordinate. No human being would click
075            // there... ;-)
076            return false;
077        }
078
079        int boxWidth = cw / NUMBER_OF_CHARS;
080        int answer = x / boxWidth;
081
082        setLastclickPosition(session, answer);
083
084        return answer == pos;
085    }
086
087    /**
088     * Compute a random set of characters, with exactly one 'X' at the given position.
089     *
090     * @param pos
091     *            position of the 'X'
092     * @return captcha text
093     */
094    private char[] computeChars(int pos) {
095        char[] chars = new char[NUMBER_OF_CHARS];
096
097        for (int ix = 0; ix < NUMBER_OF_CHARS; ix++) {
098            if (ix == pos) {
099                chars[ix] = 'X';
100            } else {
101                chars[ix] = CHARSET.charAt(rnd.nextInt(CHARSET.length()));
102            }
103        }
104
105        return chars;
106    }
107
108    /**
109     * Computes the position of the correct captcha answer.
110     * <p>
111     * Makes sure the new correct answer is never at the same position as the previous
112     * click. This will keep spammers from just clicking at the same position until they
113     * gave the right answer by lucky chance.
114     *
115     * @param session
116     *            {@link HttpSession} with captcha data
117     * @return position of the correct answer
118     */
119    private int computeCaptchaPosition(HttpSession session) {
120        int newPos;
121
122        Integer oldPos = getLastclickPosition(session);
123        if (oldPos != null) {
124            // Make sure newPos is always != oldPos
125            newPos = rnd.nextInt(NUMBER_OF_CHARS - 1);
126            if (newPos >= oldPos) newPos++;
127        } else {
128            newPos = rnd.nextInt(NUMBER_OF_CHARS);
129        }
130
131        setCaptchaPosition(session, newPos);
132        return newPos;
133    }
134
135    /**
136     * Gets the last captcha position from the session.
137     *
138     * @param session
139     *            {@link HttpSession}
140     * @return the last captcha position, or {@code null} if there is none yet
141     */
142    private Integer getCaptchaPosition(HttpSession session) {
143        return (Integer) session.getAttribute(CAPTCHA_NAME);
144    }
145
146    /**
147     * Sets the captcha position.
148     *
149     * @param session
150     *            {@link HttpSession}
151     * @param pos
152     *            captcha position to store
153     */
154    private void setCaptchaPosition(HttpSession session, int pos) {
155        session.setAttribute(CAPTCHA_NAME, pos);
156    }
157
158    /**
159     * Gets the position of the last click.
160     *
161     * @param session
162     *            {@link HttpSession}
163     * @return position of the last click, or {@code null} if the user did not click yet
164     */
165    private Integer getLastclickPosition(HttpSession session) {
166        return (Integer) session.getAttribute(LASTCLICK_NAME);
167    }
168
169    /**
170     * Sets the position of the last click.
171     *
172     * @param session
173     *            {@link HttpSession}
174     * @param pos
175     *            position of the last click
176     */
177    private void setLastclickPosition(HttpSession session, int pos) {
178        session.setAttribute(LASTCLICK_NAME, pos);
179    }
180
181}