001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation.tests;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.net.IDN;
007import java.util.regex.Pattern;
008
009import org.openstreetmap.josm.command.ChangePropertyCommand;
010import org.openstreetmap.josm.data.osm.Node;
011import org.openstreetmap.josm.data.osm.OsmPrimitive;
012import org.openstreetmap.josm.data.osm.Relation;
013import org.openstreetmap.josm.data.osm.Way;
014import org.openstreetmap.josm.data.validation.FixableTestError;
015import org.openstreetmap.josm.data.validation.Severity;
016import org.openstreetmap.josm.data.validation.Test;
017import org.openstreetmap.josm.data.validation.TestError;
018import org.openstreetmap.josm.data.validation.routines.AbstractValidator;
019import org.openstreetmap.josm.data.validation.routines.EmailValidator;
020import org.openstreetmap.josm.data.validation.routines.UrlValidator;
021
022/**
023 * Performs validation tests on internet-related tags (websites, e-mail addresses, etc.).
024 * @since 7489
025 */
026public class InternetTags extends Test {
027
028    /** Error code for an invalid URL */
029    public static final int INVALID_URL = 3301;
030    /** Error code for an invalid e-mail */
031    public static final int INVALID_EMAIL = 3302;
032
033    private static final Pattern ASCII_PATTERN = Pattern.compile("^\\p{ASCII}+$");
034
035    /**
036     * List of keys subject to URL validation.
037     */
038    private static String[] URL_KEYS = new String[] {
039        "url", "source:url",
040        "website", "contact:website", "heritage:website", "source:website"
041    };
042
043    /**
044     * List of keys subject to email validation.
045     */
046    private static String[] EMAIL_KEYS = new String[] {
047        "email", "contact:email"
048    };
049
050    /**
051     * Constructs a new {@code InternetTags} test.
052     */
053    public InternetTags() {
054        super(tr("Internet tags"), tr("Checks for errors in internet-related tags."));
055    }
056
057    /**
058     * Potentially validates a given primitive key against a given validator.
059     * @param p The OSM primitive to test
060     * @param k The key to validate
061     * @param keys The list of keys to check. If {@code k} is not inside this collection, do nothing
062     * @param validator The validator to run if {@code k} is inside {@code keys}
063     * @param code The error code to set if the validation fails
064     * @return {@code true} if the validation fails. In this case, a new error has been created.
065     */
066    private boolean doTest(OsmPrimitive p, String k, String[] keys, AbstractValidator validator, int code) {
067        for (String i : keys) {
068            if (i.equals(k)) {
069                TestError error = validateTag(p, k, validator, code);
070                if (error != null) {
071                    errors.add(error);
072                }
073                break;
074            }
075        }
076        return false;
077    }
078
079    /**
080     * Validates a given primitive tag against a given validator.
081     * @param p The OSM primitive to test
082     * @param k The key to validate
083     * @param validator The validator to run
084     * @param code The error code to set if the validation fails
085     * @return The error if the validation fails, {@code null} otherwise
086     * @since 7824
087     */
088    public TestError validateTag(OsmPrimitive p, String k, AbstractValidator validator, int code) {
089        TestError error = doValidateTag(p, k, null, validator, code);
090        if (error != null) {
091            // Workaround to https://issues.apache.org/jira/browse/VALIDATOR-290
092            // Apache Commons Validator 1.4.1-SNAPSHOT does not support yet IDN URLs
093            // To remove if it gets fixed on Apache side
094            String v = p.get(k);
095            if (!ASCII_PATTERN.matcher(v).matches()) {
096                try {
097                    String protocol = "";
098                    if (v.contains("://")) {
099                        protocol = v.substring(0, v.indexOf("://")+3);
100                    }
101                    String domain = !protocol.isEmpty() ? v.substring(protocol.length(), v.length()) : v;
102                    String ending = "";
103                    if (domain.contains("/")) {
104                        int idx = domain.indexOf("/");
105                        ending = domain.substring(idx, domain.length());
106                        domain = domain.substring(0, idx);
107                    }
108                    // Try to apply ToASCII algorithm
109                    error = doValidateTag(p, k, protocol+IDN.toASCII(domain)+ending, validator, code);
110                } catch (IllegalArgumentException e) {
111                    error.setMessage(error.getMessage() +
112                            tr(" URL cannot be converted to ASCII: {0}", e.getMessage()));
113                }
114            }
115        }
116        return error;
117    }
118
119    /**
120     * Validates a given primitive tag against a given validator.
121     * @param p The OSM primitive to test
122     * @param k The key to validate
123     * @param v The value to validate. May be {@code null} to use {@code p.get(k)}
124     * @param validator The validator to run
125     * @param code The error code to set if the validation fails
126     * @return The error if the validation fails, {@code null} otherwise
127     */
128    private TestError doValidateTag(OsmPrimitive p, String k, String v, AbstractValidator validator, int code) {
129        TestError error = null;
130        String value = v != null ? v : p.get(k);
131        if (!validator.isValid(value)) {
132            String errMsg = validator.getErrorMessage();
133            // Special treatment to allow URLs without protocol. See UrlValidator#isValid
134            if (tr("URL contains an invalid protocol: {0}", (String)null).equals(errMsg)) {
135                String proto = validator instanceof EmailValidator ? "mailto://" : "http://";
136                return doValidateTag(p, k, proto+value, validator, code);
137            }
138            String msg = tr("''{0}'': {1}", k, errMsg);
139            String fix = validator.getFix();
140            if (fix != null) {
141                error = new FixableTestError(this, Severity.WARNING, msg, code, p,
142                        new ChangePropertyCommand(p, k, fix));
143            } else {
144                error = new TestError(this, Severity.WARNING, msg, code, p);
145            }
146        }
147        return error;
148    }
149
150    private void test(OsmPrimitive p) {
151        for (String k : p.keySet()) {
152            // Test key against URL validator
153            if (!doTest(p, k, URL_KEYS, UrlValidator.getInstance(), INVALID_URL)) {
154                // Test key against e-mail validator only if the URL validator did not fail
155                doTest(p, k, EMAIL_KEYS, EmailValidator.getInstance(), INVALID_EMAIL);
156            }
157        }
158    }
159
160    @Override
161    public void visit(Node n) {
162        test(n);
163    }
164
165    @Override
166    public void visit(Way w) {
167        test(w);
168    }
169
170    @Override
171    public void visit(Relation r) {
172        test(r);
173    }
174}