    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.validator;
    018
    019import java.io.Serializable;
    020import java.util.Arrays;
    021import java.util.HashSet;
    022import java.util.Set;
    023import java.util.regex.Matcher;
    024import java.util.regex.Pattern;
    025
    026import org.apache.commons.validator.routines.InetAddressValidator;
    027import org.apache.commons.validator.util.Flags;
    028
    029/**
    030 * <p>Validates URLs.</p>
    031 * Behavour of validation is modified by passing in options:
    032 * <ul>
    033 * <li>ALLOW_2_SLASHES - [FALSE]  Allows double '/' characters in the path
    034 * component.</li>
    035 * <li>NO_FRAGMENT- [FALSE]  By default fragments are allowed, if this option is
    036 * included then fragments are flagged as illegal.</li>
    037 * <li>ALLOW_ALL_SCHEMES - [FALSE] By default only http, https, and ftp are
    038 * considered valid schemes.  Enabling this option will let any scheme pass validation.</li>
    039 * </ul>
    040 *
    041 * <p>Originally based in on php script by Debbie Dyer, validation.php v1.2b, Date: 03/07/02,
    042 * http://javascript.internet.com. However, this validation now bears little resemblance
    043 * to the php original.</p>
    044 * <pre>
    045 *   Example of usage:
    046 *   Construct a UrlValidator with valid schemes of "http", and "https".
    047 *
    048 *    String[] schemes = {"http","https"}.
    049 *    UrlValidator urlValidator = new UrlValidator(schemes);
    050 *    if (urlValidator.isValid("ftp://foo.bar.com/")) {
    051 *       System.out.println("url is valid");
    052 *    } else {
    053 *       System.out.println("url is invalid");
    054 *    }
    055 *
    056 *    prints "url is invalid"
    057 *   If instead the default constructor is used.
    058 *
    059 *    UrlValidator urlValidator = new UrlValidator();
    060 *    if (urlValidator.isValid("ftp://foo.bar.com/")) {
    061 *       System.out.println("url is valid");
    062 *    } else {
    063 *       System.out.println("url is invalid");
    064 *    }
    065 *
    066 *   prints out "url is valid"
    067 *  </pre>
    068 *
    069 * @see
    070 * <a href="http://www.ietf.org/rfc/rfc2396.txt">
    071 *  Uniform Resource Identifiers (URI): Generic Syntax
    072 * </a>
    073 *
    074 * @version $Revision$
    075 * @since Validator 1.1
    076 * @deprecated Use the new UrlValidator in the routines package. This class
    077 * will be removed in a future release.
    078 */
    079@Deprecated
    080public class UrlValidator implements Serializable {
    081
    082    private static final long serialVersionUID = 24137157400029593L;
    083
    084    /**
    085     * Allows all validly formatted schemes to pass validation instead of
    086     * supplying a set of valid schemes.
    087     */
    088    public static final int ALLOW_ALL_SCHEMES = 1 << 0;
    089
    090    /**
    091     * Allow two slashes in the path component of the URL.
    092     */
    093    public static final int ALLOW_2_SLASHES = 1 << 1;
    094
    095    /**
    096     * Enabling this options disallows any URL fragments.
    097     */
    098    public static final int NO_FRAGMENTS = 1 << 2;
    099
    100    private static final String ALPHA_CHARS = "a-zA-Z";
    101
    102// NOT USED   private static final String ALPHA_NUMERIC_CHARS = ALPHA_CHARS + "\\d";
    103
    104    private static final String SPECIAL_CHARS = ";/@&=,.?:+$";
    105
    106    private static final String VALID_CHARS = "[^\\s" + SPECIAL_CHARS + "]";
    107
    108    // Drop numeric, and  "+-." for now
    109    private static final String AUTHORITY_CHARS_REGEX = "\\p{Alnum}\\-\\.";
    110
    111    private static final String ATOM = VALID_CHARS + '+';
    112
    113    /**
    114     * This expression derived/taken from the BNF for URI (RFC2396).
    115     */
    116    private static final String URL_REGEX =
    117            "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?";
    118    //                                                                      12            3  4          5       6   7        8 9
    119    private static final Pattern URL_PATTERN = Pattern.compile(URL_REGEX);
    120
    121    /**
    122     * Schema/Protocol (ie. http:, ftp:, file:, etc).
    123     */
    124    private static final int PARSE_URL_SCHEME = 2;
    125
    126    /**
    127     * Includes hostname/ip and port number.
    128     */
    129    private static final int PARSE_URL_AUTHORITY = 4;
    130
    131    private static final int PARSE_URL_PATH = 5;
    132
    133    private static final int PARSE_URL_QUERY = 7;
    134
    135    private static final int PARSE_URL_FRAGMENT = 9;
    136
    137    /**
    138     * Protocol (ie. http:, ftp:,https:).
    139     */
    140    private static final Pattern SCHEME_PATTERN = Pattern.compile("^\\p{Alpha}[\\p{Alnum}\\+\\-\\.]*");
    141
    142    private static final String AUTHORITY_REGEX =
    143       "^([" + AUTHORITY_CHARS_REGEX + "]*)(:\\d*)?(.*)?";
    144    //                                                                            1                          2  3       4
    145    private static final Pattern AUTHORITY_PATTERN = Pattern.compile(AUTHORITY_REGEX);
    146
    147    private static final int PARSE_AUTHORITY_HOST_IP = 1;
    148
    149    private static final int PARSE_AUTHORITY_PORT = 2;
    150
    151    /**
    152     * Should always be empty.
    153     */
    154    private static final int PARSE_AUTHORITY_EXTRA = 3;
    155
    156    private static final Pattern PATH_PATTERN = Pattern.compile("^(/[-\\w:@&?=+,.!/~*'%$_;]*)?$");
    157
    158    private static final Pattern QUERY_PATTERN = Pattern.compile("^(.*)$");
    159
    160    private static final Pattern LEGAL_ASCII_PATTERN = Pattern.compile("^\\p{ASCII}+$");
    161
    162    private static final Pattern DOMAIN_PATTERN =
    163            Pattern.compile("^" + ATOM + "(\\." + ATOM + ")*$");
    164
    165    private static final Pattern PORT_PATTERN = Pattern.compile("^:(\\d{1,5})$");
    166
    167    private static final Pattern ATOM_PATTERN = Pattern.compile("^(" + ATOM + ").*?$");
    168
    169    private static final Pattern ALPHA_PATTERN = Pattern.compile("^[" + ALPHA_CHARS + "]");
    170
    171    /**
    172     * Holds the set of current validation options.
    173     */
    174    private final Flags options;
    175
    176    /**
    177     * The set of schemes that are allowed to be in a URL.
    178     */
    179    private final Set<String> allowedSchemes = new HashSet<String>();
    180
    181    /**
    182     * If no schemes are provided, default to this set.
    183     */
    184    protected String[] defaultSchemes = {"http", "https", "ftp"};
    185
    186    /**
    187     * Create a UrlValidator with default properties.
    188     */
    189    public UrlValidator() {
    190        this(null);
    191    }
    192
    193    /**
    194     * Behavior of validation is modified by passing in several strings options:
    195     * @param schemes Pass in one or more url schemes to consider valid, passing in
    196     *        a null will default to "http,https,ftp" being valid.
    197     *        If a non-null schemes is specified then all valid schemes must
    198     *        be specified. Setting the ALLOW_ALL_SCHEMES option will
    199     *        ignore the contents of schemes.
    200     */
    201    public UrlValidator(String[] schemes) {
    202        this(schemes, 0);
    203    }
    204
    205    /**
    206     * Initialize a UrlValidator with the given validation options.
    207     * @param options The options should be set using the public constants declared in
    208     * this class.  To set multiple options you simply add them together.  For example,
    209     * ALLOW_2_SLASHES + NO_FRAGMENTS enables both of those options.
    210     */
    211    public UrlValidator(int options) {
    212        this(null, options);
    213    }
    214
    215    /**
    216     * Behavour of validation is modified by passing in options:
    217     * @param schemes The set of valid schemes.
    218     * @param options The options should be set using the public constants declared in
    219     * this class.  To set multiple options you simply add them together.  For example,
    220     * ALLOW_2_SLASHES + NO_FRAGMENTS enables both of those options.
    221     */
    222    public UrlValidator(String[] schemes, int options) {
    223        this.options = new Flags(options);
    224
    225        if (this.options.isOn(ALLOW_ALL_SCHEMES)) {
    226            return;
    227        }
    228
    229        if (schemes == null) {
    230            schemes = this.defaultSchemes;
    231        }
    232
    233        this.allowedSchemes.addAll(Arrays.asList(schemes));
    234    }
    235
    236    /**
    237     * <p>Checks if a field has a valid url address.</p>
    238     *
    239     * @param value The value validation is being performed on.  A <code>null</code>
    240     * value is considered invalid.
    241     * @return true if the url is valid.
    242     */
    243    public boolean isValid(String value) {
    244        if (value == null) {
    245            return false;
    246        }
    247        if (!LEGAL_ASCII_PATTERN.matcher(value).matches()) {
    248           return false;
    249        }
    250
    251        // Check the whole url address structure
    252        Matcher urlMatcher = URL_PATTERN.matcher(value);
    253        if (!urlMatcher.matches()) {
    254            return false;
    255        }
    256
    257        if (!isValidScheme(urlMatcher.group(PARSE_URL_SCHEME))) {
    258            return false;
    259        }
    260
    261        if (!isValidAuthority(urlMatcher.group(PARSE_URL_AUTHORITY))) {
    262            return false;
    263        }
    264
    265        if (!isValidPath(urlMatcher.group(PARSE_URL_PATH))) {
    266            return false;
    267        }
    268
    269        if (!isValidQuery(urlMatcher.group(PARSE_URL_QUERY))) {
    270            return false;
    271        }
    272
    273        if (!isValidFragment(urlMatcher.group(PARSE_URL_FRAGMENT))) {
    274            return false;
    275        }
    276
    277        return true;
    278    }
    279
    280    /**
    281     * Validate scheme. If schemes[] was initialized to a non null,
    282     * then only those scheme's are allowed.  Note this is slightly different
    283     * than for the constructor.
    284     * @param scheme The scheme to validate.  A <code>null</code> value is considered
    285     * invalid.
    286     * @return true if valid.
    287     */
    288    protected boolean isValidScheme(String scheme) {
    289        if (scheme == null) {
    290            return false;
    291        }
    292
    293        if (!SCHEME_PATTERN.matcher(scheme).matches()) {
    294            return false;
    295        }
    296
    297        if (options.isOff(ALLOW_ALL_SCHEMES) && !allowedSchemes.contains(scheme)) {
    298            return false;
    299        }
    300
    301        return true;
    302    }
    303
    304    /**
    305     * Returns true if the authority is properly formatted.  An authority is the combination
    306     * of hostname and port.  A <code>null</code> authority value is considered invalid.
    307     * @param authority Authority value to validate.
    308     * @return true if authority (hostname and port) is valid.
    309     */
    310    protected boolean isValidAuthority(String authority) {
    311        if (authority == null) {
    312            return false;
    313        }
    314
    315        InetAddressValidator inetAddressValidator =
    316                InetAddressValidator.getInstance();
    317
    318        Matcher authorityMatcher = AUTHORITY_PATTERN.matcher(authority);
    319        if (!authorityMatcher.matches()) {
    320            return false;
    321        }
    322
    323        boolean hostname = false;
    324        // check if authority is IP address or hostname
    325        String hostIP = authorityMatcher.group(PARSE_AUTHORITY_HOST_IP);
    326        boolean ipV4Address = inetAddressValidator.isValid(hostIP);
    327
    328        if (!ipV4Address) {
    329            // Domain is hostname name
    330            hostname = DOMAIN_PATTERN.matcher(hostIP).matches();
    331        }
    332
    333        //rightmost hostname will never start with a digit.
    334        if (hostname) {
    335            // LOW-TECH FIX FOR VALIDATOR-202
    336            // TODO: Rewrite to use ArrayList and .add semantics: see VALIDATOR-203
    337            char[] chars = hostIP.toCharArray();
    338            int size = 1;
    339            for(int i=0; i<chars.length; i++) {
    340                if(chars[i] == '.') {
    341                    size++;
    342                }
    343            }
    344            String[] domainSegment = new String[size];
    345            boolean match = true;
    346            int segmentCount = 0;
    347            int segmentLength = 0;
    348
    349            while (match) {
    350                Matcher atomMatcher = ATOM_PATTERN.matcher(hostIP);
    351                match = atomMatcher.matches();
    352                if (match) {
    353                    domainSegment[segmentCount] = atomMatcher.group(1);
    354                    segmentLength = domainSegment[segmentCount].length() + 1;
    355                    hostIP =
    356                            (segmentLength >= hostIP.length())
    357                            ? ""
    358                            : hostIP.substring(segmentLength);
    359
    360                    segmentCount++;
    361                }
    362            }
    363            String topLevel = domainSegment[segmentCount - 1];
    364            if (topLevel.length() < 2 || topLevel.length() > 4) { // CHECKSTYLE IGNORE MagicNumber (deprecated code)
    365                return false;
    366            }
    367
    368            // First letter of top level must be a alpha
    369            if (!ALPHA_PATTERN.matcher(topLevel.substring(0, 1)).matches()) {
    370                return false;
    371            }
    372
    373            // Make sure there's a host name preceding the authority.
    374            if (segmentCount < 2) {
    375                return false;
    376            }
    377        }
    378
    379        if (!hostname && !ipV4Address) {
    380            return false;
    381        }
    382
    383        String port = authorityMatcher.group(PARSE_AUTHORITY_PORT);
    384        if (port != null && !PORT_PATTERN.matcher(port).matches()) {
    385            return false;
    386        }
    387
    388        String extra = authorityMatcher.group(PARSE_AUTHORITY_EXTRA);
    389        if (!GenericValidator.isBlankOrNull(extra)) {
    390            return false;
    391        }
    392
    393        return true;
    394    }
    395
    396    /**
    397     * Returns true if the path is valid.  A <code>null</code> value is considered invalid.
    398     * @param path Path value to validate.
    399     * @return true if path is valid.
    400     */
    401    protected boolean isValidPath(String path) {
    402        if (path == null) {
    403            return false;
    404        }
    405
    406        if (!PATH_PATTERN.matcher(path).matches()) {
    407            return false;
    408        }
    409
    410        int slash2Count = countToken("//", path);
    411        if (options.isOff(ALLOW_2_SLASHES) && (slash2Count > 0)) {
    412            return false;
    413        }
    414
    415        int slashCount = countToken("/", path);
    416        int dot2Count = countToken("..", path);
    417        if (dot2Count > 0 && (slashCount - slash2Count - 1) <= dot2Count){
    418            return false;
    419        }
    420
    421        return true;
    422    }
    423
    424    /**
    425     * Returns true if the query is null or it's a properly formatted query string.
    426     * @param query Query value to validate.
    427     * @return true if query is valid.
    428     */
    429    protected boolean isValidQuery(String query) {
    430        if (query == null) {
    431            return true;
    432        }
    433
    434        return QUERY_PATTERN.matcher(query).matches();
    435    }
    436
    437    /**
    438     * Returns true if the given fragment is null or fragments are allowed.
    439     * @param fragment Fragment value to validate.
    440     * @return true if fragment is valid.
    441     */
    442    protected boolean isValidFragment(String fragment) {
    443        if (fragment == null) {
    444            return true;
    445        }
    446
    447        return options.isOff(NO_FRAGMENTS);
    448    }
    449
    450    /**
    451     * Returns the number of times the token appears in the target.
    452     * @param token Token value to be counted.
    453     * @param target Target value to count tokens in.
    454     * @return the number of tokens.
    455     */
    456    protected int countToken(String token, String target) {
    457        int tokenIndex = 0;
    458        int count = 0;
    459        while (tokenIndex != -1) {
    460            tokenIndex = target.indexOf(token, tokenIndex);
    461            if (tokenIndex > -1) {
    462                tokenIndex++;
    463                count++;
    464            }
    465        }
    466        return count;
    467    }
    468}