You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jspwiki.apache.org by aj...@apache.org on 2008/02/13 06:54:24 UTC

svn commit: r627255 [21/41] - in /incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src: ./ com/ com/ecyrd/ com/ecyrd/jspwiki/ com/ecyrd/jspwiki/action/ com/ecyrd/jspwiki/attachment/ com/ecyrd/jspwiki/auth/ com/ecyrd/jspwiki/auth/acl/ com/ecyrd/jspwiki...

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/filters/SpamFilter.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/filters/SpamFilter.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/filters/SpamFilter.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/filters/SpamFilter.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,1140 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2005 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki.filters;
+
+import java.io.*;
+import java.util.*;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.jsp.PageContext;
+
+import net.sf.akismet.Akismet;
+
+import org.apache.commons.jrcs.diff.*;
+import org.apache.commons.jrcs.diff.myers.MyersDiff;
+import org.apache.commons.lang.time.StopWatch;
+import org.apache.log4j.Logger;
+import org.apache.oro.text.regex.*;
+
+import com.ecyrd.jspwiki.*;
+import com.ecyrd.jspwiki.action.CommentActionBean;
+import com.ecyrd.jspwiki.action.NoneActionBean;
+import com.ecyrd.jspwiki.action.ViewActionBean;
+import com.ecyrd.jspwiki.attachment.Attachment;
+import com.ecyrd.jspwiki.auth.user.UserProfile;
+import com.ecyrd.jspwiki.providers.ProviderException;
+import com.ecyrd.jspwiki.ui.EditorManager;
+import com.ecyrd.jspwiki.ui.WikiInterceptor;
+
+/**
+ *  This is Herb, the JSPWiki spamfilter that can also do choke modifications.
+ *
+ *  Parameters:
+ *  <ul>
+ *    <li>wordlist - Page name where the regexps are found.  Use [{SET spamwords='regexp list separated with spaces'}] on
+ *     that page.  Default is "SpamFilterWordList".
+ *    <li>blacklist - The name of an attachment containing the list of spam patterns, one per line. Default is
+ *        "SpamFilterWordList/blacklist.txt"</li>
+ *    <li>errorpage - The page to which the user is redirected.  Has a special variable $msg which states the reason. Default is "RejectedMessage".
+ *    <li>pagechangesinminute - How many page changes are allowed/minute.  Default is 5.</li>
+ *    <li>similarchanges - How many similar page changes are allowed before the host is banned.  Default is 2.  (since 2.4.72)</li>
+ *    <li>bantime - How long an IP address stays on the temporary ban list (default is 60 for 60 minutes).</li>
+ *    <li>maxurls - How many URLs can be added to the page before it is considered spam (default is 5)</li>
+ *    <li>akismet-apikey - The Akismet API key (see akismet.org)</li>
+ *    <li>ignoreauthenticated - If set to "true", all authenticated users are ignored and never caught in SpamFilter</li>
+ *    <li>captcha - Sets the captcha technology to use.  Current allowed values are "none" and "asirra".</li>
+ *    <li>strategy - Sets the filtering strategy to use.  If set to "eager", will stop at the first probable
+ *        match, and won't consider any other tests.  This is the default, as it's considerably lighter. If set to "score", will go through all of the tests
+ *        and calculates a score for the spam, which is then compared to a filter level value.
+ *  </ul>
+ *
+ *  <p>Changes by admin users are ignored in any case.</p>
+ *
+ *  @since 2.1.112
+ *  @author Janne Jalkanen
+ */
+public class SpamFilter
+    extends BasicPageFilter
+{
+    private static final String ATTR_SPAMFILTER_SCORE = "spamfilter.score";
+    private static final String REASON_REGEXP = "Regexp";
+    private static final String REASON_IP_BANNED_TEMPORARILY = "IPBannedTemporarily";
+    private static final String REASON_BOT_TRAP = "BotTrap";
+    private static final String REASON_AKISMET = "Akismet";
+    private static final String REASON_TOO_MANY_URLS = "TooManyUrls";
+    private static final String REASON_SIMILAR_MODIFICATIONS = "SimilarModifications";
+    private static final String REASON_TOO_MANY_MODIFICATIONS = "TooManyModifications";
+    private static final String REASON_UTF8_TRAP = "UTF8Trap";
+
+    private static final String LISTVAR = "spamwords";
+    public static final String  PROP_WORDLIST              = "wordlist";
+    public static final String  PROP_ERRORPAGE             = "errorpage";
+    public static final String  PROP_PAGECHANGES           = "pagechangesinminute";
+    public static final String  PROP_SIMILARCHANGES        = "similarchanges";
+    public static final String  PROP_BANTIME               = "bantime";
+    public static final String  PROP_BLACKLIST             = "blacklist";
+    public static final String  PROP_MAXURLS               = "maxurls";
+    public static final String  PROP_AKISMET_API_KEY       = "akismet-apikey";
+    public static final String  PROP_IGNORE_AUTHENTICATED  = "ignoreauthenticated";
+    public static final String  PROP_CAPTCHA               = "captcha";
+    public static final String  PROP_FILTERSTRATEGY        = "strategy";
+
+    public static final String  STRATEGY_EAGER             = "eager";
+    public static final String  STRATEGY_SCORE             = "score";
+
+    private static final String URL_REGEXP = "(http://|https://|mailto:)([A-Za-z0-9_/\\.\\+\\?\\#\\-\\@=&;]+)";
+
+    private String          m_forbiddenWordsPage = "SpamFilterWordList";
+    private String          m_errorPage          = "RejectedMessage";
+    private String          m_blacklist          = "SpamFilterWordList/blacklist.txt";
+
+    private PatternMatcher  m_matcher = new Perl5Matcher();
+    private PatternCompiler m_compiler = new Perl5Compiler();
+
+    private Collection      m_spamPatterns = null;
+
+    private Date            m_lastRebuild = new Date( 0L );
+
+    static  Logger          spamlog = Logger.getLogger( "SpamLog" );
+    static  Logger          log = Logger.getLogger( SpamFilter.class );
+
+
+    private Vector          m_temporaryBanList = new Vector();
+
+    private int             m_banTime = 60; // minutes
+
+    private Vector          m_lastModifications = new Vector();
+
+    /**
+     *  How many times a single IP address can change a page per minute?
+     */
+    private int             m_limitSinglePageChanges = 5;
+
+    /**
+     *  How many times can you add the exact same string to a page?
+     */
+    private int             m_limitSimilarChanges = 2;
+
+    /**
+     *  How many URLs can be added at maximum.
+     */
+    private int             m_maxUrls = 10;
+
+    private Pattern         m_urlPattern;
+    private Akismet         m_akismet;
+
+    private String          m_akismetAPIKey = null;
+
+    private boolean         m_useCaptcha = false;
+
+    /** The limit at which we consider something to be spam. */
+    private int             m_scoreLimit = 1;
+
+    /**
+     * If set to true, will ignore anyone who is in Authenticated role.
+     */
+    private boolean         m_ignoreAuthenticated = false;
+
+    private boolean         m_stopAtFirstMatch = true;
+
+    public void initialize( WikiEngine engine, Properties properties )
+    {
+        m_forbiddenWordsPage = properties.getProperty( PROP_WORDLIST,
+                                                       m_forbiddenWordsPage );
+        m_errorPage = properties.getProperty( PROP_ERRORPAGE,
+                                              m_errorPage );
+
+        m_limitSinglePageChanges = TextUtil.getIntegerProperty( properties,
+                                                                PROP_PAGECHANGES,
+                                                                m_limitSinglePageChanges );
+
+        m_limitSimilarChanges = TextUtil.getIntegerProperty( properties,
+                                                             PROP_SIMILARCHANGES,
+                                                             m_limitSimilarChanges );
+
+        m_maxUrls = TextUtil.getIntegerProperty( properties,
+                                                 PROP_MAXURLS,
+                                                 m_maxUrls );
+
+        m_banTime = TextUtil.getIntegerProperty( properties,
+                                                 PROP_BANTIME,
+                                                 m_banTime );
+
+        m_blacklist = properties.getProperty( PROP_BLACKLIST, m_blacklist );
+
+        m_ignoreAuthenticated = TextUtil.getBooleanProperty( properties,
+                                                             PROP_IGNORE_AUTHENTICATED,
+                                                             m_ignoreAuthenticated );
+
+        m_useCaptcha = properties.getProperty( PROP_CAPTCHA, "" ).equals("asirra");
+
+        try
+        {
+            m_urlPattern = m_compiler.compile( URL_REGEXP );
+        }
+        catch( MalformedPatternException e )
+        {
+            log.fatal("Internal error: Someone put in a faulty pattern.",e);
+            throw new InternalWikiException("Faulty pattern.");
+        }
+
+        m_akismetAPIKey = TextUtil.getStringProperty( properties,
+                                                      PROP_AKISMET_API_KEY,
+                                                      m_akismetAPIKey );
+
+        m_stopAtFirstMatch = TextUtil.getStringProperty( properties,
+                                                         PROP_FILTERSTRATEGY,
+                                                         STRATEGY_EAGER ).equals(STRATEGY_EAGER);
+
+        log.info("# Spam filter initialized.  Temporary ban time "+m_banTime+
+                 " mins, max page changes/minute: "+m_limitSinglePageChanges );
+
+
+    }
+
+    private static final int REJECT = 0;
+    private static final int ACCEPT = 1;
+    private static final int NOTE   = 2;
+
+    private static String log( WikiContext ctx, int type, String source, String message )
+    {
+        message = TextUtil.replaceString( message, "\r\n", "\\r\\n" );
+        message = TextUtil.replaceString( message, "\"", "\\\"" );
+
+        String uid = getUniqueID();
+
+        String page   = ctx.getPage().getName();
+        String reason = "UNKNOWN";
+        String addr   = ctx.getHttpRequest() != null ? ctx.getHttpRequest().getRemoteAddr() : "-";
+
+        switch( type )
+        {
+            case REJECT:
+                reason = "REJECTED";
+                break;
+            case ACCEPT:
+                reason = "ACCEPTED";
+                break;
+            case NOTE:
+                reason = "NOTE";
+                break;
+            default:
+                throw new InternalWikiException("Illegal type "+type);
+        }
+
+        spamlog.info( reason+" "+source+" "+uid+" "+addr+" \""+page+"\" "+message );
+
+        return uid;
+    }
+
+
+    public String preSave( WikiContext context, String content )
+        throws RedirectException
+    {
+        cleanBanList();
+        refreshBlacklists(context);
+
+        String change = getChange( context, content );
+
+        if(!ignoreThisUser(context))
+        {
+            checkBanList( context, change );
+            checkSinglePageChange( context, content, change );
+            checkPatternList(context, content, change);
+        }
+
+        if( !m_stopAtFirstMatch )
+        {
+            Integer score = (Integer)context.getVariable(ATTR_SPAMFILTER_SCORE);
+
+            if( score != null && score.intValue() >= m_scoreLimit )
+            {
+                throw new RedirectException( "Herb says you got too many points",
+                                             getRedirectPage(context) );
+            }
+        }
+
+        log( context, ACCEPT, "-", change );
+        return content;
+    }
+
+    private void checkStrategy( WikiContext context, String error, String message )
+        throws RedirectException
+    {
+        if( m_stopAtFirstMatch )
+        {
+            throw new RedirectException( message, getRedirectPage(context) );
+        }
+
+        Integer score = (Integer)context.getVariable( ATTR_SPAMFILTER_SCORE );
+
+        if( score != null )
+            score = new Integer( score.intValue()+1 );
+        else
+            score = new Integer( 1 );
+
+        context.setVariable( ATTR_SPAMFILTER_SCORE, score );
+    }
+    /**
+     *  Parses a list of patterns and returns a Collection of compiled Pattern
+     *  objects.
+     *
+     * @param source
+     * @param list
+     * @return
+     */
+    private Collection parseWordList( WikiPage source, String list )
+    {
+        ArrayList compiledpatterns = new ArrayList();
+
+        if( list != null )
+        {
+            StringTokenizer tok = new StringTokenizer( list, " \t\n" );
+
+            while( tok.hasMoreTokens() )
+            {
+                String pattern = tok.nextToken();
+
+                try
+                {
+                    compiledpatterns.add( m_compiler.compile( pattern ) );
+                }
+                catch( MalformedPatternException e )
+                {
+                    log.debug( "Malformed spam filter pattern "+pattern );
+
+                    source.setAttribute("error", "Malformed spam filter pattern "+pattern);
+                }
+            }
+        }
+
+        return compiledpatterns;
+    }
+
+    /**
+     *  Takes a MT-Blacklist -formatted blacklist and returns a list of compiled
+     *  Pattern objects.
+     *
+     *  @param list
+     *  @return
+     */
+    private Collection parseBlacklist( String list )
+    {
+        ArrayList compiledpatterns = new ArrayList();
+
+        if( list != null )
+        {
+            try
+            {
+                BufferedReader in = new BufferedReader( new StringReader(list) );
+
+                String line;
+
+                while( (line = in.readLine()) != null )
+                {
+                    line = line.trim();
+                    if( line.length() == 0 ) continue; // Empty line
+                    if( line.startsWith("#") ) continue; // It's a comment
+
+                    int ws = line.indexOf(' ');
+
+                    if( ws == -1 ) ws = line.indexOf('\t');
+
+                    if( ws != -1 ) line = line.substring(0,ws);
+
+                    try
+                    {
+                        compiledpatterns.add( m_compiler.compile( line ) );
+                    }
+                    catch( MalformedPatternException e )
+                    {
+                        log.debug( "Malformed spam filter pattern "+line );
+                    }
+                }
+            }
+            catch( IOException e )
+            {
+                log.info("Could not read patterns; returning what I got",e);
+            }
+        }
+
+        return compiledpatterns;
+    }
+
+    /**
+     *  Takes a single page change and performs a load of tests on the content change.
+     *  An admin can modify anything.
+     *
+     *  @param context
+     *  @param content
+     *  @throws RedirectException
+     */
+    private synchronized void checkSinglePageChange( WikiContext context, String content, String change )
+        throws RedirectException
+    {
+        HttpServletRequest req = context.getHttpRequest();
+
+        if( req != null )
+        {
+            String addr = req.getRemoteAddr();
+            int hostCounter = 0;
+            int changeCounter = 0;
+
+            log.debug("Change is "+change);
+
+            long time = System.currentTimeMillis()-60*1000L; // 1 minute
+
+            for( Iterator i = m_lastModifications.iterator(); i.hasNext(); )
+            {
+                Host host = (Host)i.next();
+
+                //
+                //  Check if this item is invalid
+                //
+                if( host.getAddedTime() < time )
+                {
+                    log.debug("Removed host "+host.getAddress()+" from modification queue (expired)");
+                    i.remove();
+                    continue;
+                }
+
+                //
+                // Check if this IP address has been seen before
+                //
+
+                if( host.getAddress().equals(addr) )
+                {
+                    hostCounter++;
+                }
+
+                //
+                //  Check, if this change has been seen before
+                //
+
+                if( host.getChange() != null && host.getChange().equals(change) )
+                {
+                    changeCounter++;
+                }
+            }
+
+            //
+            //  Now, let's check against the limits.
+            //
+            if( hostCounter >= m_limitSinglePageChanges )
+            {
+                Host host = new Host( addr, null );
+
+                m_temporaryBanList.add( host );
+
+                String uid = log( context, REJECT, REASON_TOO_MANY_MODIFICATIONS, change );
+                log.info( "SPAM:TooManyModifications ("+uid+"). Added host "+addr+" to temporary ban list for doing too many modifications/minute" );
+                checkStrategy( context, REASON_TOO_MANY_MODIFICATIONS, "Herb says you look like a spammer, and I trust Herb! (Incident code "+uid+")" );
+            }
+
+            if( changeCounter >= m_limitSimilarChanges )
+            {
+                Host host = new Host( addr, null );
+
+                m_temporaryBanList.add( host );
+
+                String uid = log( context, REJECT, REASON_SIMILAR_MODIFICATIONS, change );
+
+                log.info("SPAM:SimilarModifications ("+uid+"). Added host "+addr+" to temporary ban list for doing too many similar modifications" );
+                checkStrategy( context, REASON_SIMILAR_MODIFICATIONS, "Herb says you look like a spammer, and I trust Herb! (Incident code "+uid+")");
+            }
+
+            //
+            //  Calculate the number of links in the addition.
+            //
+
+            String tstChange = change;
+            int    urlCounter = 0;
+
+            while( m_matcher.contains(tstChange,m_urlPattern) )
+            {
+                MatchResult m = m_matcher.getMatch();
+
+                tstChange = tstChange.substring( m.endOffset(0) );
+
+                urlCounter++;
+            }
+
+            if( urlCounter > m_maxUrls )
+            {
+                Host host = new Host( addr, null );
+
+                m_temporaryBanList.add( host );
+
+                String uid = log( context, REJECT, REASON_TOO_MANY_URLS, change );
+
+                log.info("SPAM:TooManyUrls ("+uid+"). Added host "+addr+" to temporary ban list for adding too many URLs" );
+                checkStrategy( context, REASON_TOO_MANY_URLS, "Herb says you look like a spammer, and I trust Herb! (Incident code "+uid+")" );
+            }
+
+            //
+            //  Check bot trap
+            //
+
+            checkBotTrap( context, change );
+
+            //
+            //  Check UTF-8 mangling
+            //
+
+            checkUTF8( context, change );
+
+            //
+            //  Do Akismet check.  This is good to be the last, because this is the most
+            //  expensive operation.
+            //
+
+            checkAkismet( context, change );
+
+            m_lastModifications.add( new Host( addr, change ) );
+        }
+    }
+
+
+    /**
+     *  Checks against the akismet system.
+     *
+     * @param context
+     * @param change
+     * @throws RedirectException
+     */
+    private void checkAkismet( WikiContext context, String change )
+        throws RedirectException
+    {
+        if( m_akismetAPIKey != null )
+        {
+            if( m_akismet == null )
+            {
+                log.info("Initializing Akismet spam protection.");
+
+                m_akismet = new Akismet( m_akismetAPIKey, context.getEngine().getBaseURL() );
+
+                if( !m_akismet.verifyAPIKey() )
+                {
+                    log.error("Akismet API key cannot be verified.  Please check your config.");
+                    m_akismetAPIKey = null;
+                    m_akismet = null;
+                }
+            }
+
+            HttpServletRequest req = context.getHttpRequest();
+
+            if( req != null && m_akismet != null )
+            {
+                log.debug("Calling Akismet to check for spam...");
+
+                StopWatch sw = new StopWatch();
+                sw.start();
+
+                String ipAddress     = req.getRemoteAddr();
+                String userAgent     = req.getHeader("User-Agent");
+                String referrer      = req.getHeader( "Referer");
+                String permalink     = context.getViewURL( context.getPage().getName() );
+                String commentType   = context instanceof CommentActionBean ? "comment" : "edit";
+                String commentAuthor = context.getCurrentUser().getName();
+                String commentAuthorEmail = null;
+                String commentAuthorURL   = null;
+
+                boolean isSpam = m_akismet.commentCheck( ipAddress,
+                                                         userAgent,
+                                                         referrer,
+                                                         permalink,
+                                                         commentType,
+                                                         commentAuthor,
+                                                         commentAuthorEmail,
+                                                         commentAuthorURL,
+                                                         change,
+                                                         null );
+
+                sw.stop();
+
+                log.debug("Akismet request done in: "+sw);
+
+                if( isSpam )
+                {
+                    // Host host = new Host( ipAddress, null );
+
+                    // m_temporaryBanList.add( host );
+
+                    String uid = log( context, REJECT, REASON_AKISMET, change );
+
+                    log.info("SPAM:Akismet ("+uid+"). Akismet thinks this change is spam; added host to temporary ban list.");
+
+                    checkStrategy( context, REASON_AKISMET, "Akismet tells Herb you're a spammer, Herb trusts Akismet, and I trust Herb! (Incident code "+uid+")");
+                }
+            }
+        }
+    }
+
+    /**
+     *  Returns a static string which can be used to detect spambots which
+     *  just wildly fill in all the fields.
+     *
+     *  @return A string
+     */
+    public static String getBotFieldName()
+    {
+        return "submit_auth";
+    }
+
+    /**
+     *  This checks whether an invisible field is available in the request, and
+     *  whether it's contents are suspected spam.
+     *
+     *  @param context
+     *  @param change
+     * @throws RedirectException
+     */
+    private void checkBotTrap( WikiContext context, String change ) throws RedirectException
+    {
+        HttpServletRequest request = context.getHttpRequest();
+
+        if( request != null )
+        {
+            String unspam = request.getParameter( getBotFieldName() );
+            if( unspam != null && unspam.length() > 0 )
+            {
+                String uid = log( context, REJECT, REASON_BOT_TRAP, change );
+
+                log.info("SPAM:BotTrap ("+uid+").  Wildly behaving bot detected.");
+
+                checkStrategy( context, REASON_BOT_TRAP, "Spamming attempt detected. (Incident code "+uid+")");
+
+            }
+        }
+    }
+
+    private void checkUTF8( WikiContext context, String change ) throws RedirectException
+    {
+        HttpServletRequest request = context.getHttpRequest();
+
+        if( request != null )
+        {
+            String utf8field = request.getParameter( "encodingcheck" );
+
+            if( utf8field != null && !utf8field.equals("\u3041") )
+            {
+                String uid = log( context, REJECT, REASON_UTF8_TRAP, change );
+
+                log.info("SPAM:UTF8Trap ("+uid+").  Wildly posting dumb bot detected.");
+
+                checkStrategy( context, REASON_UTF8_TRAP, "Spamming attempt detected. (Incident code "+uid+")");
+            }
+        }
+    }
+
+    /**
+     *  Goes through the ban list and cleans away any host which has expired from it.
+     */
+    private synchronized void cleanBanList()
+    {
+        long now = System.currentTimeMillis();
+
+        for( Iterator i = m_temporaryBanList.iterator(); i.hasNext(); )
+        {
+            Host host = (Host)i.next();
+
+            if( host.getReleaseTime() < now )
+            {
+                log.debug("Removed host "+host.getAddress()+" from temporary ban list (expired)");
+                i.remove();
+            }
+        }
+    }
+
+    /**
+     *  Checks the ban list if the IP address of the changer is already on it.
+     *
+     *  @param context
+     *  @throws RedirectException
+     */
+
+    private void checkBanList( WikiContext context, String change )
+        throws RedirectException
+    {
+        HttpServletRequest req = context.getHttpRequest();
+
+        if( req != null )
+        {
+            String remote = req.getRemoteAddr();
+
+            long now = System.currentTimeMillis();
+
+            for( Iterator i = m_temporaryBanList.iterator(); i.hasNext(); )
+            {
+                Host host = (Host)i.next();
+
+                if( host.getAddress().equals(remote) )
+                {
+                    long timeleft = (host.getReleaseTime() - now) / 1000L;
+
+                    log( context, REJECT, REASON_IP_BANNED_TEMPORARILY, change );
+
+                    checkStrategy( context, REASON_IP_BANNED_TEMPORARILY, "You have been temporarily banned from modifying this wiki. ("+timeleft+" seconds of ban left)");
+                }
+            }
+        }
+
+    }
+
+    /**
+     *  If the spam filter notices changes in the black list page, it will refresh
+     *  them automatically.
+     *
+     *  @param context
+     */
+    private void refreshBlacklists( WikiContext context )
+    {
+        try
+        {
+            WikiPage source = context.getEngine().getPage( m_forbiddenWordsPage );
+            Attachment att = context.getEngine().getAttachmentManager().getAttachmentInfo( context, m_blacklist );
+
+            boolean rebuild = false;
+
+            //
+            //  Rebuild, if the page or the attachment has changed since.
+            //
+            if( source != null )
+            {
+                if( m_spamPatterns == null || m_spamPatterns.isEmpty() || source.getLastModified().after(m_lastRebuild) )
+                {
+                    rebuild = true;
+                }
+            }
+
+            if( att != null )
+            {
+                if( m_spamPatterns == null || m_spamPatterns.isEmpty() || att.getLastModified().after(m_lastRebuild) )
+                {
+                    rebuild = true;
+                }
+            }
+
+
+            //
+            //  Do the actual rebuilding.  For simplicity's sake, we always rebuild the complete
+            //  filter list regardless of what changed.
+            //
+
+            if( rebuild )
+            {
+                m_lastRebuild = new Date();
+
+                m_spamPatterns = parseWordList( source,
+                                                (source != null) ? (String)source.getAttribute( LISTVAR ) : null );
+
+                log.info("Spam filter reloaded - recognizing "+m_spamPatterns.size()+" patterns from page "+m_forbiddenWordsPage);
+
+                if( att != null )
+                {
+                    InputStream in = context.getEngine().getAttachmentManager().getAttachmentStream(att);
+
+                    StringWriter out = new StringWriter();
+
+                    FileUtil.copyContents( new InputStreamReader(in,"UTF-8"), out );
+
+                    Collection blackList = parseBlacklist( out.toString() );
+
+                    log.info("...recognizing additional "+blackList.size()+" patterns from blacklist "+m_blacklist);
+
+                    m_spamPatterns.addAll( blackList );
+                }
+            }
+        }
+        catch( IOException ex )
+        {
+            log.info("Unable to read attachment data, continuing...",ex);
+        }
+        catch( ProviderException ex )
+        {
+            log.info("Failed to read spam filter attachment, continuing...",ex);
+        }
+
+    }
+
+    /**
+     *  Does a check against a known pattern list.
+     *
+     *  @param context
+     *  @param content
+     *  @param change
+     *  @throws RedirectException
+     */
+    private void checkPatternList(WikiContext context, String content, String change) throws RedirectException
+    {
+        //
+        //  If we have no spam patterns defined, or we're trying to save
+        //  the page containing the patterns, just return.
+        //
+        if( m_spamPatterns == null || context.getPage().getName().equals( m_forbiddenWordsPage ) )
+        {
+            return;
+        }
+
+        if( context.getHttpRequest() != null )
+            change += context.getHttpRequest().getRemoteAddr();
+
+        for( Iterator i = m_spamPatterns.iterator(); i.hasNext(); )
+        {
+            Pattern p = (Pattern) i.next();
+
+            // log.debug("Attempting to match page contents with "+p.getPattern());
+
+            if( m_matcher.contains( change, p ) )
+            {
+                //
+                //  Spam filter has a match.
+                //
+                String uid = log( context, REJECT, REASON_REGEXP+"("+p.getPattern()+")", change);
+
+                log.info("SPAM:Regexp ("+uid+"). Content matches the spam filter '"+p.getPattern()+"'");
+
+                checkStrategy( context, REASON_REGEXP, "Herb says '"+p.getPattern()+"' is a bad spam word and I trust Herb! (Incident code "+uid+")");
+            }
+        }
+    }
+
+    /**
+     *  Creates a simple text string describing the added content.
+     *
+     *  @param context
+     *  @param newText
+     *  @return Empty string, if there is no change.
+     */
+    private static String getChange( WikiContext context, String newText )
+    {
+        WikiPage page = context.getPage();
+        StringBuffer change = new StringBuffer();
+        WikiEngine engine = context.getEngine();
+        // Get current page version
+
+        try
+        {
+            String oldText = engine.getPureText(page.getName(), WikiProvider.LATEST_VERSION);
+
+            String[] first  = Diff.stringToArray(oldText);
+            String[] second = Diff.stringToArray(newText);
+            Revision rev = Diff.diff(first, second, new MyersDiff());
+
+            if( rev == null || rev.size() == 0 )
+            {
+                return "";
+            }
+
+
+            for( int i = 0; i < rev.size(); i++ )
+            {
+                Delta d = rev.getDelta(i);
+
+                if( d instanceof AddDelta )
+                {
+                    d.getRevised().toString( change, "", "\r\n" );
+                }
+                else if( d instanceof ChangeDelta )
+                {
+                    d.getRevised().toString( change, "", "\r\n" );
+                }
+            }
+        }
+        catch (DifferentiationFailedException e)
+        {
+            log.error( "Diff failed", e );
+        }
+
+        //
+        //  Don't forget to include the change note, too
+        //
+        String changeNote = (String)page.getAttribute(WikiPage.CHANGENOTE);
+
+        if( changeNote != null )
+        {
+            change.append("\r\n");
+            change.append(changeNote);
+        }
+
+        //
+        //  And author as well
+        //
+
+        if( page.getAuthor() != null )
+        {
+            change.append("\r\n"+page.getAuthor());
+        }
+
+        return change.toString();
+    }
+
+    /**
+     *  Returns true, if this user should be ignored.
+     *
+     * @param context
+     * @return
+     */
+    private boolean ignoreThisUser(WikiContext context)
+    {
+        if( context.hasAdminPermissions() )
+        {
+            return true;
+        }
+
+        if( m_ignoreAuthenticated && context.getWikiSession().isAuthenticated() )
+        {
+            return true;
+        }
+
+        if( context.getVariable("captcha") != null )
+        {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     *  Returns a random string of six uppercase characters.
+     *
+     *  @return A random string
+     */
+    private static String getUniqueID()
+    {
+        StringBuffer sb = new StringBuffer();
+        Random rand = new Random();
+
+        for( int i = 0; i < 6; i++ )
+        {
+            char x = (char)('A'+rand.nextInt(26));
+
+            sb.append(x);
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     *  Returns a page to which we shall redirect, based on the current value
+     *  of the "captcha" parameter.
+     *
+     *  @param ctx WikiContext
+     *  @return An URL to redirect to
+     */
+    private String getRedirectPage( WikiContext ctx )
+    {
+        Map<String,String> urlParams = new HashMap<String,String>();
+        if( m_useCaptcha )
+        {
+            urlParams.put( "page", ctx.getEngine().encodeName( ctx.getPage().getName() ) );
+            return ctx.getContext().getURL( NoneActionBean.class, "Captcha.jsp", urlParams );
+        }
+
+        return ctx.getContext().getURL( ViewActionBean.class, m_errorPage);
+    }
+
+    /**
+     *  Checks whether the UserProfile matches certain checks.
+     *
+     *  @param profile
+     *  @return
+     *  @since 2.6.1
+     */
+    public boolean isValidUserProfile( WikiContext context, UserProfile profile )
+    {
+        try
+        {
+            checkPatternList( context, profile.getEmail(), profile.getEmail() );
+            checkPatternList( context, profile.getFullname(), profile.getFullname() );
+            checkPatternList( context, profile.getLoginName(), profile.getLoginName() );
+        }
+        catch( RedirectException e )
+        {
+            log.info("Detected attempt to create a spammer user account (see above for rejection reason)");
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     *  This method is used to calculate an unique code when submitting the page
+     *  to detect edit conflicts.  It currently incorporates the last-modified
+     *  date of the page, and the IP address of the submitter.
+     *
+     *  @param page The WikiPage under edit
+     *  @param request The HTTP Request
+     *  @since 2.6
+     *  @return A hash value for this page and session
+     */
+    public static final String getSpamHash( WikiPage page, HttpServletRequest request )
+    {
+        long lastModified = 0;
+
+        if( page.getLastModified() != null )
+            lastModified = page.getLastModified().getTime();
+
+        long remote = request.getRemoteAddr().hashCode();
+
+        return Long.toString( lastModified ^ remote );
+    }
+
+    /**
+     *  Returns the name of the hash field to be used in this request.
+     *  The value is unique per session, and once the session has expired,
+     *  you cannot edit anymore.
+     *
+     *  @param request The page request
+     *  @return The name to be used in the hash field
+     *  @since  2.6
+     */
+
+    private static String c_hashName;
+    private static long   c_lastUpdate;
+
+    /** The HASH_DELAY value is a maximum amount of time that an user can keep
+     *  a session open, because after the value has expired, we will invent a new
+     *  hash field name.  By default this is {@value} hours, which should be ample
+     *  time for someone.
+     */
+    private static final long HASH_DELAY = 24;
+
+    public static final String getHashFieldName( HttpServletRequest request )
+    {
+        String hash = null;
+
+        if( request.getSession() != null )
+        {
+            hash = (String)request.getSession().getAttribute("_hash");
+
+            if( hash == null )
+            {
+                hash = c_hashName;
+
+                request.getSession().setAttribute( "_hash", hash );
+            }
+        }
+
+        if( c_hashName == null || c_lastUpdate < (System.currentTimeMillis() - HASH_DELAY*60*60*1000) )
+        {
+            c_hashName = getUniqueID().toLowerCase();
+
+            c_lastUpdate = System.currentTimeMillis();
+        }
+
+        return hash != null ? hash : c_hashName;
+    }
+
+
+    /**
+     *  This method checks if the hash value is still valid, i.e. if it exists at all. This
+     *  can occur in two cases: either this is a spam bot which is not adaptive, or it is
+     *  someone who has been editing one page for too long, and their session has expired.
+     *  <p>
+     *  This method puts a redirect to the http response field to page "SessionExpired"
+     *  and logs the incident in the spam log (it may or may not be spam, but it's rather likely
+     *  that it is).
+     *
+     *  @param context
+     *  @param pageContext
+     *  @return True, if hash is okay.  False, if hash is not okay, and you need to redirect.
+     *  @throws IOException If redirection fails
+     *  @since 2.6
+     */
+    public static final boolean checkHash( WikiContext context, PageContext pageContext )
+        throws IOException
+    {
+        String hashName = getHashFieldName( (HttpServletRequest)pageContext.getRequest() );
+
+        if( pageContext.getRequest().getParameter(hashName) == null )
+        {
+            if( pageContext.getAttribute( hashName ) == null )
+            {
+                String change = getChange( context, EditorManager.getEditedText( pageContext ) );
+
+                log( context, REJECT, "MissingHash", change );
+
+                String redirect = context.getContext().getURL( ViewActionBean.class, "SessionExpired");
+                ((HttpServletResponse)pageContext.getResponse()).sendRedirect( redirect );
+
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    public static final String insertInputFields( PageContext pageContext )
+    {
+        WikiEngine engine = (WikiEngine)pageContext.getAttribute( WikiInterceptor.ATTR_WIKIENGINE, PageContext.REQUEST_SCOPE );
+
+        StringBuffer sb = new StringBuffer();
+        if (engine.getContentEncoding().equals("UTF-8"))
+        {
+            sb.append("<input name='encodingcheck' type='hidden' value='\u3041' />\n");
+        }
+
+        return sb.toString();
+    }
+    /**
+     *  A local class for storing host information.
+     *
+     *  @author jalkanen
+     *
+     *  @since
+     */
+    private class Host
+    {
+        private  long m_addedTime = System.currentTimeMillis();
+        private  long m_releaseTime;
+        private  String m_address;
+        private  String m_change;
+
+        public String getAddress()
+        {
+            return m_address;
+        }
+
+        public long getReleaseTime()
+        {
+            return m_releaseTime;
+        }
+
+        public long getAddedTime()
+        {
+            return m_addedTime;
+        }
+
+        public String getChange()
+        {
+            return m_change;
+        }
+
+        public Host( String ipaddress, String change )
+        {
+            m_address = ipaddress;
+            m_change  = change;
+
+            m_releaseTime = System.currentTimeMillis() + m_banTime * 60 * 1000L;
+        }
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormClose.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormClose.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormClose.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormClose.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,66 @@
+/*
+    WikiForms - a WikiPage FORM handler for JSPWiki.
+ 
+    Copyright (C) 2003 BaseN. 
+
+    JSPWiki Copyright (C) 2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+ 
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+ 
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+*/
+package com.ecyrd.jspwiki.forms;
+
+import com.ecyrd.jspwiki.*;
+import com.ecyrd.jspwiki.plugin.PluginException;
+import com.ecyrd.jspwiki.plugin.WikiPlugin;
+
+import java.util.*;
+
+/**
+ *  Closes a WikiForm.
+ *
+ *  @author ebu
+ */
+public class FormClose
+    extends FormElement
+{
+    /**
+     * Builds a Form close tag. Removes any information on the form from
+     * the WikiContext.
+     */
+    public String execute( WikiContext ctx, Map params )
+        throws PluginException
+    {
+        StringBuffer tags = new StringBuffer();
+        tags.append( "</form>\n" );
+        tags.append( "</div>" );
+
+        // Don't render if no error and error-only-rendering is on.
+        FormInfo info = getFormInfo( ctx );
+        if( info != null )
+        {
+            if( info.hide() )
+            {
+                ResourceBundle rb = ctx.getBundle(WikiPlugin.CORE_PLUGINS_RESOURCEBUNDLE);
+                return( "<p>" + rb.getString( "formclose.noneedtoshow" ) + "</p>" );
+            }
+        }
+
+        // Get rid of remaining form data, so it doesn't mess up other forms.
+        // After this, it is safe to add other Forms.
+        storeFormInfo( ctx, null );
+
+        return tags.toString();
+
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormElement.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormElement.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormElement.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormElement.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,100 @@
+/*
+    WikiForms - a WikiPage FORM handler for JSPWiki.
+ 
+    Copyright (C) 2003 BaseN. 
+
+    JSPWiki Copyright (C) 2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+ 
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+ 
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+*/
+package com.ecyrd.jspwiki.forms;
+
+import com.ecyrd.jspwiki.*;
+import com.ecyrd.jspwiki.plugin.WikiPlugin;
+
+/**
+ */
+public abstract class FormElement
+    implements WikiPlugin
+{
+    /**
+     * In order to make the form-to-handler parameter transfer easier,
+     * we prefix all user-specified FORM element names with HANDLERPARAM_PREFIX
+     * the HTML output. This lets us differentiate user-defined FormHandler
+     * parameters from Form parameters.
+     * The submit handler must then use MapUtil.requestToMap() to
+     * strip them before executing the handler itself.
+     */
+    public static final String HANDLERPARAM_PREFIX = "nbf_";
+
+    /**
+     * The submit servlet may decide to store a FormInfo with user-entered
+     * form values in the Session. It should use this name, and this class
+     * checks for it to pre-fill fields from a previous form submit.
+     */
+    public static final String FORM_VALUES_CARRIER = "nbpf_values";
+
+    // Show values:
+    public static final String HIDE_SUCCESS = "onsuccess";
+
+    // Parameter names:
+    /** Plugin parameter, optional, indicates servlet to post to. */
+    public static final String PARAM_SUBMITHANDLER = "submit";
+    /** Plugin parameter, mandatory, indicates what form element to insert. */
+    public static final String PARAM_ELEMENT    = "element";
+    /** 
+     * Plugin parameter, mandatory in output element, indicates
+     * WikiPlugin to use to handle form submitted data.
+     */
+    public static final String PARAM_HANDLER    = "handler";
+    /** Plugin parameter, mandatory in open/output: name of the form. */
+    public static final String PARAM_FORM       = "form";
+    /** Plugin parameter, mandatory in input elements: name of an element. */
+    public static final String PARAM_INPUTNAME       = "name";
+    /** Plugin parameter, optional: default value for an input. */
+    public static final String PARAM_VALUE      = "value";
+    /** Experimental: hide the form if it was submitted successfully. */ 
+    public static final String PARAM_HIDEFORM   = "hide";
+
+    /** If set to 'handler' in output element, the handler plugin is
+     * called even on first invocation (no submit). The plugin can
+     * then place values into its parameter map, and these are seen by
+     * subsequent Form elements. (Store a value in the plugin with the
+     * same key as an input element, and the value will be visible in
+     * the initial form.)
+     */
+    public static final String PARAM_POPULATE = "populate";
+    /** HTTP parameter, inserted as hidden variable into the generated form. */
+    public static final String PARAM_FORMNAMEHIDDEN   = "formname";
+
+    // Key to store the form info container in the context by:
+    //public static final String CONTEXT_FORMINFO = "FormPluginInfo";
+
+    /**
+     * Utility method stores a FormInfo object into the WikiContext.
+     */
+    protected void storeFormInfo( WikiContext ctx, FormInfo info )
+    {
+        ctx.setVariable( FORM_VALUES_CARRIER, info );
+    }
+
+    /**
+     * Attempts to retrieve information on the currently handled
+     * form from the WikiContext.
+     */
+    protected FormInfo getFormInfo( WikiContext ctx )
+    {
+        return (FormInfo)ctx.getVariable( FORM_VALUES_CARRIER );
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormHandler.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormHandler.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormHandler.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormHandler.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,36 @@
+/*
+    WikiForms - a WikiPage FORM handler for JSPWiki.
+ 
+    Copyright (C) 2003 BaseN. 
+
+    JSPWiki Copyright (C) 2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+ 
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+ 
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+*/
+package com.ecyrd.jspwiki.forms;
+
+
+/**
+ * A FormHandler performs logic based on input from an
+ * HTTP FORM, transmitted through a JSPWiki WikiPlugin
+ * (see Form.java).
+ * 
+ * <P>This interface is currently empty and unused. It acts
+ * as a place holder: we probably want to switch from 
+ * WikiPlugins to FormHandlers in Form.java, to enforce
+ * authentication, form execution permissions, and so on.
+ */
+public interface FormHandler
+{
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormInfo.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormInfo.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormInfo.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormInfo.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,160 @@
+/*
+    WikiForms - a WikiPage FORM handler for JSPWiki.
+
+    Copyright (C) 2003 BaseN.
+
+    JSPWiki Copyright (C) 2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+*/
+package com.ecyrd.jspwiki.forms;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Container for carrying HTTP FORM information between
+ * WikiPlugin invocations in the Session.
+ *
+ *  @author ebu
+ */
+public class FormInfo
+    implements Serializable
+{
+    private static final long serialVersionUID = 0L;
+
+    public static final int EXECUTED =  1;
+    public static final int OK       =  0;
+    public static final int ERROR    = -1;
+
+    public int    m_status;
+    public boolean m_hide;
+    public String m_action;
+    public String m_name;
+    public String m_handler;
+    public String m_result;
+    public String m_error;
+    //public PluginParameters submission;
+    public Map m_submission;
+
+    public FormInfo()
+    {
+        m_status = OK;
+    }
+
+    public void setStatus( int val )
+    {
+        m_status = val;
+    }
+
+    public int getStatus()
+    {
+        return m_status;
+    }
+
+    public void setHide( boolean val )
+    {
+        m_hide = val;
+    }
+
+    public boolean hide()
+    {
+        return m_hide;
+    }
+
+    public void setAction( String val )
+    {
+        m_action = val;
+    }
+
+    public String getAction()
+    {
+        return m_action;
+    }
+
+    public void setName( String val )
+    {
+        m_name = val;
+    }
+
+    public String getName()
+    {
+        return m_name;
+    }
+
+    public void setHandler( String val )
+    {
+        m_handler = val;
+    }
+
+    public String getHandler()
+    {
+        return m_handler;
+    }
+
+    public void setResult( String val )
+    {
+        m_result = val;
+    }
+
+    public String getResult()
+    {
+        return m_result;
+    }
+
+    public void setError( String val )
+    {
+        m_error = val;
+    }
+
+    public String getError()
+    {
+        return m_error;
+    }
+
+    /**
+     * Copies the given values into the handler parameter map using Map.putAll().
+     * @param val parameter name-value pairs for a Form handler WikiPlugin
+     */
+    public void setSubmission( Map val )
+    {
+        m_submission = new HashMap();
+        m_submission.putAll( val );
+    }
+
+    /**
+     * Adds the given values into the handler parameter map.
+     * @param val parameter name-value pairs for a Form handler WikiPlugin
+     */
+    public void addSubmission( Map val )
+    {
+        if( m_submission == null )
+            m_submission = new HashMap();
+        m_submission.putAll( val );
+    }
+
+    /**
+     * Returns parameter name-value pairs for a Form handler WikiPlugin.
+     * The names are those of Form input fields, and the values whatever
+     * the user selected in the form. The FormSet plugin can also be used
+     * to provide initial values.
+     *
+     * @return Handler parameter name-value pairs.
+     */
+    public Map getSubmission()
+    {
+        return m_submission;
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormInput.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormInput.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormInput.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormInput.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,92 @@
+/*
+    WikiForms - a WikiPage FORM handler for JSPWiki.
+ 
+    Copyright (C) 2003 BaseN. 
+
+    JSPWiki Copyright (C) 2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+ 
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+ 
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+*/
+package com.ecyrd.jspwiki.forms;
+
+import com.ecyrd.jspwiki.*;
+import com.ecyrd.jspwiki.plugin.PluginException;
+import java.util.*;
+
+import org.apache.ecs.xhtml.input;
+
+/**
+ *  Creates a simple input text field.
+ */
+public class FormInput
+    extends FormElement
+{
+    public static final String PARAM_TYPE  = "type";
+    public static final String PARAM_SIZE  = "size";
+
+    /**
+     * Generates a dynamic form element on the WikiPage.
+     */
+    public String execute( WikiContext ctx, Map params )
+        throws PluginException
+    {
+        String inputName  = (String)params.get( PARAM_INPUTNAME );
+        String inputValue = (String)params.get( PARAM_VALUE );
+        String inputType  = (String)params.get( PARAM_TYPE );
+        String size       = (String)params.get( PARAM_SIZE );
+
+        if( inputName == null )
+            throw new PluginException( "Input element is missing parameter 'name'." );
+        if( inputValue == null )
+            inputValue = "";
+
+        // Don't render if no error and error-only-rendering is on.
+        FormInfo info = getFormInfo( ctx );
+        Map previousValues = null;
+        if( info != null )
+        {
+            if( info.hide() )
+            {
+                return( "<p>(no need to show input field now)</p>" );
+            }
+            previousValues = info.getSubmission();
+        }
+
+        if( previousValues == null )
+        {
+            previousValues = new HashMap();
+        }
+
+        // In order to isolate posted form elements into their own
+        // map, prefix the variable name here. It will be stripped
+        // when the handler plugin is executed.
+        input field = new input( inputType, 
+                                 HANDLERPARAM_PREFIX + inputName, 
+                                 inputValue );
+
+        String checked = (String)params.get("checked");
+        field.setChecked( TextUtil.isPositive(checked)
+                          || "checked".equalsIgnoreCase(checked) );
+        
+        String oldValue = (String)previousValues.get( inputName );
+        if( oldValue != null )
+        {
+            field.setValue( oldValue );
+        }
+
+        if( size != null ) field.setSize( size );
+
+        return field.toString(ctx.getEngine().getContentEncoding());
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormOpen.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormOpen.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormOpen.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormOpen.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,152 @@
+/*
+    WikiForms - a WikiPage FORM handler for JSPWiki.
+
+    Copyright (C) 2003 BaseN.
+
+    JSPWiki Copyright (C) 2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+*/
+package com.ecyrd.jspwiki.forms;
+
+import java.text.MessageFormat;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+import com.ecyrd.jspwiki.*;
+import com.ecyrd.jspwiki.action.ViewActionBean;
+import com.ecyrd.jspwiki.plugin.PluginException;
+import com.ecyrd.jspwiki.plugin.WikiPlugin;
+
+/**
+ *  Opens a WikiForm.
+ *
+ * Builds the HTML code for opening a FORM.
+ *
+ * <p>Since we're only providing an opening FORM tag, we can't use
+ * the ECS utilities.
+ *
+ * A Form plugin line that produces one looks like this:
+ * <p><pre>
+ *   [{FormOpen name='formname' handler='pluginname'
+ *          submit='submitservlet'
+ *          show='always'
+ *   }]
+ * </pre>
+ *
+ * <p>Mandatory parameters:
+ * <br>The <i>name</i> field identifies this particular form to the
+ * Form plugin across pages.
+ * <br>The <i>handler</i> field is a WikiPlugin name; it will be
+ * invoked with the form field values.
+ *
+ * <p>Optional parameters:
+ * <p>The submitservlet is the name of a JSP/servlet capable of
+ * handling the input from this form. It is optional; the default
+ * value is the current page (which can handle the input by using
+ * this Plugin.)
+ *
+ * <p>The <i>hide</i> parameter affects the visibility of this
+ * form. If left out, the form is always shown. If set to
+ * 'onsuccess', the form is not shown if it was submitted
+ * successfully. (Note that a reload of the page would cause the
+ * context to reset, and the form would be shown again. This may
+ * be a useless option.)
+ *
+ *  @author ebu
+ */
+public class FormOpen
+    extends FormElement
+{
+    private static org.apache.log4j.Logger log =
+        org.apache.log4j.Logger.getLogger( FormOpen.class );
+
+    public static final String PARAM_METHOD = "method";
+
+    /**
+     */
+    public String execute( WikiContext ctx, Map params )
+        throws PluginException
+    {
+        ResourceBundle rb = ctx.getBundle(WikiPlugin.CORE_PLUGINS_RESOURCEBUNDLE);
+        String formName = (String)params.get( PARAM_FORM );
+        if( formName == null )
+        {
+            Object[] args = { PARAM_FORM };
+            throw new PluginException( MessageFormat.format( rb.getString( "formopen.missingparam" ), args ) );
+        }
+        String hide     = (String)params.get( PARAM_HIDEFORM );
+        String sourcePage = ctx.getPage().getName();
+        String submitServlet = (String)params.get( PARAM_SUBMITHANDLER );
+        if( submitServlet == null )
+            submitServlet = ctx.getContext().getURL( ViewActionBean.class, sourcePage );
+
+        String method = (String)params.get( PARAM_METHOD );
+        if( method == null ) method="post";
+
+        if( !(method.equalsIgnoreCase("get") || method.equalsIgnoreCase("post")) )
+        {
+            throw new PluginException( rb.getString( "formopen.postorgetonly" ) );
+        }
+
+        FormInfo info = getFormInfo( ctx );
+        if( info != null )
+        {
+            // Previous information may be the result of submitting
+            // this form, or of a FormSet plugin, or both. If it
+            // exists and is for this form, fine.
+            if( formName.equals( info.getName() ) )
+            {
+                log.debug( "Previous FormInfo for this form was found in context." );
+                // If the FormInfo exists, and if we're supposed to display on
+                // error only, we need to exit now.
+                if( hide != null &&
+                    HIDE_SUCCESS.equals( hide ) &&
+                    info.getStatus() == FormInfo.EXECUTED )
+                {
+                    info.setHide( true );
+                    return( "<p>" + rb.getString( "formopen.noneedtoshow" ) + "</p>" );
+                }
+            }
+            else
+            {
+                // This would mean that a new form was started without
+                // closing an old one.  Get rid of the garbage.
+                info = new FormInfo();
+            }
+        }
+        else
+        {
+            // No previous FormInfo available; store now, so it'll be
+            // available for upcoming Form input elements.
+            info = new FormInfo();
+            storeFormInfo( ctx, info );
+        }
+
+        info.setName( formName );
+        info.setAction( submitServlet );
+
+        StringBuffer tag = new StringBuffer( 40 );
+        tag.append( "<div class=\"wikiform\">\n" );
+        tag.append( "<form action=\"" + submitServlet );
+        tag.append( "\" name=\"" + formName );
+        tag.append( "\" accept-charset=\"" + ctx.getEngine().getContentEncoding() );
+        tag.append( "\" method=\""+method+"\" enctype=\"application/x-www-form-urlencoded\">\n" );
+        tag.append( "  <input type=\"hidden\" name=\"" + PARAM_FORMNAMEHIDDEN );
+        tag.append( "\" value=\"" + formName + "\"/>\n" );
+
+        return tag.toString();
+    }
+
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormOutput.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormOutput.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormOutput.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormOutput.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,149 @@
+/*
+    WikiForms - a WikiPage FORM handler for JSPWiki.
+ 
+    Copyright (C) 2003 BaseN. 
+
+    JSPWiki Copyright (C) 2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+ 
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+ 
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+*/
+package com.ecyrd.jspwiki.forms;
+
+import java.text.MessageFormat;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+import com.ecyrd.jspwiki.*;
+import com.ecyrd.jspwiki.action.ViewActionBean;
+import com.ecyrd.jspwiki.plugin.PluginException;
+import com.ecyrd.jspwiki.plugin.PluginManager;
+import com.ecyrd.jspwiki.plugin.WikiPlugin;
+import com.ecyrd.jspwiki.util.FormUtil;
+
+/**
+ */
+public class FormOutput
+    extends FormElement
+{
+    /**
+     * Executes the FormHandler specified in a Form 'output' plugin,
+     * using entries provided in the HttpRequest as FormHandler
+     * parameters.
+     * <p>
+     * If the parameter 'populate' was given, the WikiPlugin it names
+     * is used to get default values. (It probably makes a lot of
+     * sense for this to be the same plugin as the handler.) 
+     * Information for the populator can be given with the FormSet
+     * plugin. If 'populate' is not specified, the form is not
+     * displayed.
+     * <p>
+     * Should there be no HTTP request associated with this request,
+     * the method will return immediately with an empty string.
+     */
+    public String execute( WikiContext ctx, Map params )
+        throws PluginException
+    {
+        //
+        //  If there is no HTTP request, returns immediately.
+        //
+        if( ctx.getHttpRequest() == null )
+        {
+            return "";
+        }
+        ResourceBundle rb = ctx.getBundle(WikiPlugin.CORE_PLUGINS_RESOURCEBUNDLE);
+        
+        // If we are NOT here due to this form being submitted, we do nothing.
+        // The submitted form MUST have parameter 'formname' equal to the name
+        // parameter of this Form plugin.
+
+        String formName   = (String)params.get( PARAM_FORM );
+        String submitForm = ctx.getHttpParameter( PARAM_FORMNAMEHIDDEN );
+        String populator  = (String)params.get( PARAM_POPULATE );
+
+        if( submitForm == null || formName == null || 
+            !formName.equals( submitForm ) )
+        {
+            // No submitForm -> this was not a submission from the
+            // generated form.  If populate is specified, we'll go
+            // ahead and let the handler (populator) put stuff into
+            // the context, otherwise we'll just hide.
+            if( populator == null || !PARAM_HANDLER.equals( populator ) )
+                return "";
+            // If population was allowed, we should first  
+        }
+
+        String handler = (String)params.get( PARAM_HANDLER );
+        if( handler == null || handler.length() == 0 )
+        {
+            Object[] args = { PARAM_HANDLER };
+            // Need to print out an error here as this form is misconfigured
+            return "<p class=\"error\">" + MessageFormat.format( rb.getString( "formoutput.missingargument" ), args ) + "</p>";
+        }
+
+        String sourcePage = ctx.getPage().getName();
+        String submitServlet = ctx.getContext().getURL( ViewActionBean.class, sourcePage );
+
+        // If there is previous FormInfo available - say, from a
+        // FormSet plugin - use it.
+        FormInfo info = getFormInfo( ctx );
+        if( info == null )
+        {
+            // Reconstruct the form info from post data
+            info = new FormInfo();
+            info.setName( formName );
+        }
+        // Force override of handler and submit.
+        info.setHandler( handler );
+        info.setAction( submitServlet );
+
+        // Sift out all extra parameters, leaving only those submitted
+        // in the HTML FORM.
+        Map handlerParams = FormUtil.requestToMap( ctx.getHttpRequest(), 
+                                                   HANDLERPARAM_PREFIX );
+        // Previous submission info may be available from FormSet
+        // plugin - add, don't replace.
+        info.addSubmission( handlerParams );
+
+        // Pass the _body parameter from FormOutput on to the handler
+        info.getSubmission().put(PluginManager.PARAM_BODY, 
+                                 params.get(PluginManager.PARAM_BODY)); 
+
+        String handlerOutput = null;
+        String error = null;
+        try
+        {
+            // The plugin _can_ modify the parameters, so we make sure
+            // they stay with us.
+            handlerOutput = ctx.getEngine().getPluginManager().execute( ctx, handler, info.getSubmission() );
+            info.setResult( handlerOutput );
+            info.setStatus( FormInfo.EXECUTED );
+        }
+        catch( PluginException pe )
+        {
+            error = "<p class=\"error\">" + pe.getMessage() + "</p>";
+            info.setError( error );
+            info.setStatus( FormInfo.ERROR );
+        }
+
+        // We store the forminfo, so following Form plugin invocations on this
+        // page can decide what to do based on its values.
+        storeFormInfo( ctx, info );
+
+        if( error != null )
+            return error;
+
+        return handlerOutput != null ? handlerOutput : "";
+    }
+
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormSelect.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormSelect.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormSelect.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormSelect.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,176 @@
+/*
+    WikiForms - a WikiPage FORM handler for JSPWiki.
+ 
+    Copyright (C) 2003 BaseN. 
+
+    JSPWiki Copyright (C) 2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+ 
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+ 
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+*/
+package com.ecyrd.jspwiki.forms;
+
+import com.ecyrd.jspwiki.*;
+import com.ecyrd.jspwiki.plugin.PluginException;
+import com.ecyrd.jspwiki.plugin.WikiPlugin;
+
+import java.util.*;
+
+import org.apache.ecs.ConcreteElement;
+import org.apache.ecs.xhtml.option;
+import org.apache.ecs.xhtml.select;
+
+/**
+ *  @author ebu
+ */
+public class FormSelect
+    extends FormElement
+{
+    public String execute( WikiContext ctx, Map params )
+        throws PluginException
+    {
+        // Don't render if no error and error-only-rendering is on.
+        FormInfo info = getFormInfo( ctx );
+
+        ResourceBundle rb = ctx.getBundle(WikiPlugin.CORE_PLUGINS_RESOURCEBUNDLE);
+        Map previousValues = null;
+        
+        if( info != null )
+        {
+            if( info.hide() )
+            {
+                return "<p>" + rb.getString( "forminput.noneedtoshow" ) + "</p>";
+            }
+            previousValues = info.getSubmission();
+        }
+
+        if( previousValues == null )
+        {
+            previousValues = new HashMap();
+        }
+
+        ConcreteElement field = null;
+        
+        field = buildSelect( params, previousValues, rb );
+
+        // We should look for extra params, e.g. width, ..., here.
+        if( field != null )
+            return field.toString(ctx.getEngine().getContentEncoding());
+        
+        return "";
+    }
+
+
+    /**
+     * Builds a Select element.
+     */
+    private select buildSelect( Map pluginParams, Map ctxValues, ResourceBundle rb )
+        throws PluginException
+    {
+        String inputName = (String)pluginParams.get( PARAM_INPUTNAME );
+        if( inputName == null )
+        {
+            throw new PluginException( rb.getString( "formselect.namemissing" ) );
+        }
+    
+        String inputValue = (String)pluginParams.get( PARAM_VALUE );
+        String previousValue = (String)ctxValues.get( inputName );
+        //
+        // We provide several ways to override the separator, in case
+        // some input application the default value.
+        //
+        String optionSeparator = (String)pluginParams.get( "separator" );
+        if( optionSeparator == null )
+            optionSeparator = (String)ctxValues.get( "separator." + inputName);
+        if( optionSeparator == null )
+            optionSeparator = (String)ctxValues.get( "select.separator" );
+        if( optionSeparator == null )
+            optionSeparator = ";";
+        
+        String optionSelector = (String)pluginParams.get( "selector" );
+        if( optionSelector == null )
+            optionSelector = (String)ctxValues.get( "selector." + inputName );
+        if( optionSelector == null )
+            optionSelector = (String)ctxValues.get( "select.selector" );
+        if( optionSelector == null )
+            optionSelector = "*";
+        if( optionSelector.equals( optionSeparator ) )
+            optionSelector = null;
+        if( inputValue == null )
+            inputValue = "";
+
+        // If values from the context contain the separator, we assume
+        // that the plugin or something else has given us a better
+        // list to display.
+        boolean contextValueOverride = false;
+        if( previousValue != null )
+        {
+            if( previousValue.indexOf( optionSeparator ) != -1 )
+            {
+                inputValue = previousValue;
+                previousValue = null;
+            }
+            else
+            {
+                // If a context value exists, but it's not a list,
+                // it'll just override any existing selector
+                // indications.
+                contextValueOverride = true;
+            }
+        }
+
+        String[] options = inputValue.split( optionSeparator );
+        if( options == null )
+            options = new String[0];
+        int previouslySelected = -1;
+        
+        option[] optionElements = new option[options.length];
+        
+        //
+        //  Figure out which one of the options to select: prefer the one
+        //  that was previously selected, otherwise try to find the one
+        //  with the "select" marker.
+        //
+        for( int i = 0; i < options.length; i++ )
+        {
+            int indicated = -1;
+            options[i] = options[i].trim();
+            
+            if( options[i].startsWith( optionSelector ) ) 
+            {
+                options[i] = options[i].substring( optionSelector.length() );
+                indicated = i;
+            }
+            if( previouslySelected == -1 )
+            {
+                if( !contextValueOverride && indicated > 0 )
+                {
+                    previouslySelected = indicated;
+                }
+                else if( previousValue != null && 
+                        options[i].equals( previousValue ) )
+                {
+                    previouslySelected = i;
+                }
+            }
+            
+            optionElements[i] = new option( options[i] );
+            optionElements[i].addElement( options[i] );
+        }
+
+        if( previouslySelected > -1 ) optionElements[previouslySelected].setSelected(true);
+        select field = new select( HANDLERPARAM_PREFIX + inputName, optionElements );
+
+        return( field );
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormSet.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormSet.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormSet.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormSet.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,94 @@
+/*
+    WikiForms - a WikiPage FORM handler for JSPWiki.
+ 
+    Copyright (C) 2003 BaseN. 
+
+    JSPWiki Copyright (C) 2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+ 
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+ 
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+*/
+package com.ecyrd.jspwiki.forms;
+
+import com.ecyrd.jspwiki.*;
+import com.ecyrd.jspwiki.plugin.*;
+import java.util.*;
+
+/**
+ * FormSet is a companion WikiPlugin for Form. 
+ * 
+ * <p>The mandatory 'form' parameter specifies which form the variable
+ * applies to.  Any other parameters are put directly into a FormInfo
+ * object that will be available to a Form plugin called 'form'
+ * (presumably invoked later on the same WikiPage).
+ * 
+ * <p>If the name of a FormSet parameter is the same as the name of
+ * a Form plugin input element later on the same page, the Form will
+ * consider the given value the default for the form field. (However,
+ * the handler for the Form is free to use the value as it wishes, and
+ * even override it.)
+ *
+ * <p>If the name of a parameter is not present in Form input fields,
+ * the parameter is presumably meant for sending initial information
+ * to the Form handler. If this is the case, you may want to specify the
+ * <i>populate=''</i> in the Form <i>open</i> element, otherwise the
+ * form won't be displayed on the first invocation.
+ *  
+ * <p>This object looks for a FormInfo object named
+ * FORM_VALUES_CARRIER in the WikiContext. If found, it checks that
+ * its name matches the 'form' parameter, and if it does, adds the
+ * plugin parameters to the FormInfo. If the names don't match, the
+ * old FormInfo is discarded and a new one is created. Only one
+ * FormInfo is supported at a time. A practical consequence of this is
+ * that a FormSet invocation only applies to the Form plugins that
+ * follow it; any further Forms need new FormSet calls.
+ *
+ * @see FormInfo
+ * @author ebu
+ */
+public class FormSet
+    implements WikiPlugin
+{    
+    public String execute( WikiContext ctx, Map params )
+        throws PluginException
+    {
+        String formName = (String)params.get( FormElement.PARAM_FORM );
+        if( formName == null || formName.trim().length() == 0 )
+        {
+            return "";
+        }
+
+        FormInfo info = (FormInfo)ctx.getVariable( FormElement.FORM_VALUES_CARRIER );
+
+        if( info == null || formName.equals( info.getName() ) == false )
+        {
+            info = new FormInfo();
+            ctx.setVariable( FormElement.FORM_VALUES_CARRIER, info );
+        }
+
+        //
+        //  Create a copy for the context.  Unfortunately we need to 
+        //  create slightly modified copy, because otherwise on next
+        //  invocation this might be coming from a cache; so we can't
+        //  modify the original param string.
+        //
+        info.setName( formName );
+        HashMap hm = new HashMap();
+        hm.putAll( params );
+        
+        hm.remove( FormElement.PARAM_FORM );
+        info.addSubmission( hm );
+        
+        return "";
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormTextarea.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormTextarea.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormTextarea.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/FormTextarea.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,105 @@
+/*
+    WikiForms - a WikiPage FORM handler for JSPWiki.
+
+    Copyright (C) 2003 BaseN.
+
+    JSPWiki Copyright (C) 2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+*/
+package com.ecyrd.jspwiki.forms;
+
+import com.ecyrd.jspwiki.*;
+import com.ecyrd.jspwiki.plugin.PluginException;
+import com.ecyrd.jspwiki.plugin.WikiPlugin;
+
+import java.util.*;
+
+import org.apache.ecs.ConcreteElement;
+import org.apache.ecs.xhtml.textarea;
+
+/**
+ *  @author ebu
+ */
+public class FormTextarea
+    extends FormElement
+{
+    public static final String PARAM_ROWS = "rows";
+    public static final String PARAM_COLS = "cols";
+
+    public String execute( WikiContext ctx, Map params )
+        throws PluginException
+    {
+        // Don't render if no error and error-only-rendering is on.
+        FormInfo info = getFormInfo( ctx );
+        Map previousValues = null;
+        ResourceBundle rb = ctx.getBundle(WikiPlugin.CORE_PLUGINS_RESOURCEBUNDLE);
+
+        if( info != null )
+        {
+            if( info.hide() )
+            {
+                return "<p>" + rb.getString( "formclose.noneedtoshow" ) + "</p>";
+            }
+            previousValues = info.getSubmission();
+        }
+
+        if( previousValues == null )
+        {
+            previousValues = new HashMap();
+        }
+
+        ConcreteElement field = null;
+
+        field = buildTextArea( params, previousValues, rb );
+
+        // We should look for extra params, e.g. width, ..., here.
+        if( field != null )
+            return field.toString( ctx.getEngine().getContentEncoding() );
+
+        return "";
+    }
+
+    private textarea buildTextArea( Map params, Map previousValues, ResourceBundle rb )
+        throws PluginException
+    {
+        String inputName = (String)params.get( PARAM_INPUTNAME );
+        String rows = (String)params.get( PARAM_ROWS );
+        String cols = (String)params.get( PARAM_COLS );
+
+        if( inputName == null )
+            throw new PluginException( rb.getString( "formtextarea.namemissing" ) );
+
+        // In order to isolate posted form elements into their own
+        // map, prefix the variable name here. It will be stripped
+        // when the handler plugin is executed.
+        textarea field = new textarea( HANDLERPARAM_PREFIX + inputName,
+                                       rows, cols);
+
+        if( previousValues != null )
+        {
+            String oldValue = (String)previousValues.get( inputName );
+            if( oldValue != null )
+            {
+                field.addElement( oldValue );
+            }
+            else
+            {
+                oldValue = (String)params.get( PARAM_VALUE );
+                if( oldValue != null ) field.addElement( oldValue );
+            }
+        }
+        return field;
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/package.html
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/package.html?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/package.html (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/forms/package.html Tue Feb 12 21:53:55 2008
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+<title>com.ecyrd.jspwiki.forms</title>
+</head>
+<body>
+
+Contains classes for doing form handling within JSPWiki.
+
+<h2>Package Specification</h2>
+
+This package contains all form-related classes, interfaces and helpers.
+
+<h2>Related Documentation</h2>
+
+</body>
+</html>
\ No newline at end of file

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/htmltowiki/ForgetNullValuesLinkedHashMap.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/htmltowiki/ForgetNullValuesLinkedHashMap.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/htmltowiki/ForgetNullValuesLinkedHashMap.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/htmltowiki/ForgetNullValuesLinkedHashMap.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,42 @@
+/* 
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki.htmltowiki;
+
+import java.util.LinkedHashMap;
+
+/**
+ * A LinkedHashMap that does not put null values into the map.
+ * 
+ * @author Sebastian Baltes (sbaltes@gmx.com)
+ */
+public class ForgetNullValuesLinkedHashMap extends LinkedHashMap
+{
+    private static final long serialVersionUID = 0L;
+    
+    public Object put( Object key, Object value )
+    {
+        if( value != null )
+        {
+            return super.put( key, value );
+        }
+        
+        return null;
+    }
+}