001/*
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.
018 */
019package org.apache.wiki.ui;
020
021import org.apache.wiki.api.core.Context;
022import org.apache.wiki.api.core.Session;
023import org.apache.wiki.i18n.InternationalizationManager;
024import org.apache.wiki.preferences.Preferences;
025
026import java.text.MessageFormat;
027import java.util.ResourceBundle;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030
031/**
032 * Provides basic validation services for HTTP parameters. Three standard validators are provided: email address, identifier and
033 * standard input. Standard input validator will reject any HTML-like input, and any of a number of special characters. ID validator
034 * rejects HTML and quoted strings, and a couple of special characters.
035 *
036 * @since 2.3.54
037 */
038public final class InputValidator {
039
040    /** Standard input validator. */
041    public static final int STANDARD = 0;
042    /** Input validator for e-mail addresses. **/
043    public static final int EMAIL    = 1;
044    /** @since 2.4.82 */
045    public static final int ID        = 2;
046
047    /**
048     * Used when checking against IDs such as a full name when saving groups.
049     * @since 2.4.82
050     */
051    static final Pattern ID_PATTERN     = Pattern.compile( "[\\x00\\r\\n\\x0f\"'<>;&\\xff{}]" );
052    static final Pattern EMAIL_PATTERN  = Pattern.compile( "^[0-9a-zA-Z-_\\.\\+]+@([0-9a-zA-Z-_]+\\.)+[a-zA-Z]+$" );
053    static final Pattern UNSAFE_PATTERN = Pattern.compile( "[\\x00\\r\\n\\x0f\"':<>\\[\\];#&@\\xff{}\\$%\\\\]" );
054
055    private final String m_form;
056    private final Session m_session;
057    private final Context m_context;
058
059    /**
060     * Constructs a new input validator for a specific form and wiki session. When validation errors are detected, they will be added to
061     * the wiki session's messages.
062     *
063     * @param form the ID or name of the form this validator should be associated with
064     * @param context the wiki context
065     */
066    public InputValidator( final String form, final Context context ) {
067        m_form = form;
068        m_context = context;
069        m_session = context.getWikiSession();
070    }
071
072    /**
073     * Validates a string against the {@link #STANDARD} validator and additionally checks that the value is not <code>null</code> or blank.
074     *
075     * @param input the string to validate
076     * @param label the label for the string or field ("E-mail address")
077     * @return returns <code>true</code> if valid, <code>false</code> otherwise
078     */
079    public boolean validateNotNull( final String input, final String label ) {
080        return validateNotNull( input, label, STANDARD );
081    }
082
083    /**
084     * Validates a string against a particular pattern type and additionally checks that the value is not <code>null</code> or blank.
085     * Delegates to {@link #validate(String, String, int)}.
086     *
087     * @param input the string to validate
088     * @param label the label for the string or field ("E-mail address")
089     * @param type the pattern type to use (<em>e.g.</em>, {@link #STANDARD}, {@link #EMAIL}.
090     * @return returns <code>true</code> if valid, <code>false</code> otherwise
091     */
092    public boolean validateNotNull( final String input, final String label, final int type ) {
093        if ( isBlank( input ) ) {
094            final ResourceBundle rb = Preferences.getBundle( m_context, InternationalizationManager.CORE_BUNDLE );
095            m_session.addMessage( m_form, MessageFormat.format( rb.getString("validate.cantbenull"), label ) );
096            return false;
097        }
098        return validate( input, label, type ) && !isBlank( input );
099    }
100
101    /**
102     * Validates a string against a particular pattern type: e-mail address, standard HTML input, etc. Note that a blank or null string will
103     * always validate.
104     *
105     * @param input the string to validate
106     * @param label the label for the string or field ("E-mail address")
107     * @param type the target pattern to validate against ({@link #STANDARD}, {@link #EMAIL})
108     * @return returns <code>true</code> if valid, <code>false</code> otherwise
109     */
110    public boolean validate( final String input, final String label, final int type ) {
111        // If blank, it's valid
112        if ( isBlank( input ) ) {
113            return true;
114        }
115
116        final ResourceBundle rb = Preferences.getBundle( m_context, InternationalizationManager.CORE_BUNDLE );
117
118        // Otherwise, see if it matches the pattern for the target type
119        final Matcher matcher;
120        final boolean valid;
121        switch( type ) {
122        case STANDARD:
123            matcher = UNSAFE_PATTERN.matcher( input );
124            valid = !matcher.find();
125            if ( !valid ) {
126                // MessageTag already invokes replaceEntities()
127                // Object[] args = { label, "&quot;&#39;&lt;&gt;;&amp;[]#\\@{}%$" };
128                final Object[] args = { label, "\'\"<>;&[]#\\@{}%$" };
129                m_session.addMessage( m_form, MessageFormat.format( rb.getString( "validate.unsafechars" ), args ) );
130            }
131            return valid;
132        case EMAIL:
133            matcher = EMAIL_PATTERN.matcher( input );
134            valid = matcher.matches();
135            if ( !valid ) {
136                final Object[] args = { label };
137                m_session.addMessage( m_form, MessageFormat.format( rb.getString( "validate.invalidemail" ), args ) );
138            }
139            return valid;
140        case ID:
141            matcher = ID_PATTERN.matcher( input );
142            valid = !matcher.find();
143            if ( !valid ) {
144                // MessageTag already invokes replaceEntities()
145                // Object[] args = { label, "&quot;&#39;&lt;&gt;;&amp;{}" };
146                final Object[] args = { label, "\'\"<>;&{}" };
147                m_session.addMessage( m_form, MessageFormat.format( rb.getString( "validate.unsafechars" ), args ) );
148            }
149            return valid;
150         default:
151             break;
152        }
153        throw new IllegalArgumentException( "Invalid input type." );
154    }
155
156    /**
157     * Returns <code>true</code> if a supplied string is null or blank
158     *
159     * @param input the string to check
160     * @return <code>true</code> if <code>null</code> or blank (zero-length); <code>false</code> otherwise
161     */
162    public static boolean isBlank( final String input ) {
163        return input == null || input.trim().length() < 1;
164    }
165
166}