001/*
002 * SonarQube
003 * Copyright (C) 2009-2017 SonarSource SA
004 * mailto:info AT sonarsource DOT com
005 *
006 * This program is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.api.web;
021
022import com.google.common.collect.ImmutableList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.HashSet;
026import java.util.LinkedHashSet;
027import java.util.List;
028import java.util.Set;
029import java.util.function.Predicate;
030import java.util.stream.Collectors;
031import javax.servlet.Filter;
032import org.sonar.api.ExtensionPoint;
033import org.sonar.api.server.ServerSide;
034
035import static com.google.common.base.Preconditions.checkArgument;
036import static java.util.Arrays.asList;
037
038/**
039 * @since 3.1
040 */
041@ServerSide
042@ExtensionPoint
043public abstract class ServletFilter implements Filter {
044
045  /**
046   * Override to change URL. Default is /*
047   */
048  public UrlPattern doGetPattern() {
049    return UrlPattern.builder().build();
050  }
051
052  public static final class UrlPattern {
053
054    private static final String MATCH_ALL = "/*";
055
056    private final List<String> inclusions;
057    private final List<String> exclusions;
058    private final Predicate<String>[] inclusionPredicates;
059    private final Predicate<String>[] exclusionPredicates;
060
061    private UrlPattern(Builder builder) {
062      this.inclusions = ImmutableList.copyOf(builder.inclusions);
063      this.exclusions = ImmutableList.copyOf(builder.exclusions);
064      if (builder.inclusionPredicates.isEmpty()) {
065        // because Stream#anyMatch() returns false if stream is empty
066        this.inclusionPredicates = new Predicate[] {s -> true};
067      } else {
068        this.inclusionPredicates = builder.inclusionPredicates.stream().toArray(Predicate[]::new);
069      }
070      this.exclusionPredicates = builder.exclusionPredicates.stream().toArray(Predicate[]::new);
071    }
072
073    public boolean matches(String path) {
074      return !Arrays.stream(exclusionPredicates).anyMatch(pattern -> pattern.test(path)) &&
075        Arrays.stream(inclusionPredicates).anyMatch(pattern -> pattern.test(path));
076    }
077
078    /**
079     * @since 6.0
080     */
081    public Collection<String> getInclusions() {
082      return inclusions;
083    }
084
085    /**
086     * @since 6.0
087     */
088    public Collection<String> getExclusions() {
089      return exclusions;
090    }
091
092    /**
093     * @deprecated replaced in version 6.0 by {@link #getInclusions()} and {@link #getExclusions()}
094     * @throws IllegalStateException if at least one exclusion or more than one inclusions are defined
095     */
096    @Deprecated
097    public String getUrl() {
098      // Before 6.0, it was only possible to include one url
099      if (exclusions.isEmpty() && inclusions.size() == 1) {
100        return inclusions.get(0);
101      }
102      throw new IllegalStateException("this method is deprecated and should not be used anymore");
103    }
104
105    public String label() {
106      return "UrlPattern{" +
107        "inclusions=[" + convertPatternsToString(inclusions) + "]" +
108        ", exclusions=[" + convertPatternsToString(exclusions) + "]" +
109        '}';
110    }
111
112    private static String convertPatternsToString(List<String> input) {
113      StringBuilder output = new StringBuilder();
114      if (input.isEmpty()) {
115        return "";
116      }
117      if (input.size() == 1) {
118        return output.append(input.get(0)).toString();
119      }
120      return output.append(input.get(0)).append(", ...").toString();
121    }
122
123    /**
124     * Defines only a single inclusion pattern. This is a shortcut for {@code builder().includes(inclusionPattern).build()}.
125     */
126    public static UrlPattern create(String inclusionPattern) {
127      return builder().includes(inclusionPattern).build();
128    }
129
130    /**
131     * @since 6.0
132     */
133    public static Builder builder() {
134      return new Builder();
135    }
136
137    /**
138     * @since 6.0
139     */
140    public static class Builder {
141      private static final String WILDCARD_CHAR = "*";
142      private static final Collection<String> STATIC_RESOURCES = ImmutableList.of("/css/*", "/fonts/*", "/images/*", "/js/*", "/static/*",
143        "/robots.txt", "/favicon.ico", "/apple-touch-icon*", "/mstile*");
144
145      private final Set<String> inclusions = new LinkedHashSet<>();
146      private final Set<String> exclusions = new LinkedHashSet<>();
147      private final Set<Predicate<String>> inclusionPredicates = new HashSet<>();
148      private final Set<Predicate<String>> exclusionPredicates = new HashSet<>();
149
150      private Builder() {
151      }
152
153      public static Collection<String> staticResourcePatterns() {
154        return STATIC_RESOURCES;
155      }
156
157      /**
158       * Add inclusion patterns. Supported formats are:
159       * <ul>
160       *   <li>path prefixed by / and ended by *, for example "/api/foo/*", to match all paths "/api/foo" and "api/api/foo/something/else"</li>
161       *   <li>path prefixed by *, for example "*\/foo", to match all paths "/api/foo" and "something/else/foo"</li>
162       *   <li>path with leading slash and no wildcard, for example "/api/foo", to match exact path "/api/foo"</li>
163       * </ul>
164       */
165      public Builder includes(String... includePatterns) {
166        return includes(asList(includePatterns));
167      }
168
169      /**
170       * Add exclusion patterns. See format described in {@link #includes(String...)}
171       */
172      public Builder includes(Collection<String> includePatterns) {
173        this.inclusions.addAll(includePatterns);
174        this.inclusionPredicates.addAll(includePatterns.stream()
175          .filter(pattern -> !MATCH_ALL.equals(pattern))
176          .map(Builder::compile)
177          .collect(Collectors.toList()));
178        return this;
179      }
180
181      public Builder excludes(String... excludePatterns) {
182        return excludes(asList(excludePatterns));
183      }
184
185      public Builder excludes(Collection<String> excludePatterns) {
186        this.exclusions.addAll(excludePatterns);
187        this.exclusionPredicates.addAll(excludePatterns.stream()
188          .map(Builder::compile)
189          .collect(Collectors.toList()));
190        return this;
191      }
192
193      public UrlPattern build() {
194        return new UrlPattern(this);
195      }
196
197      private static Predicate<String> compile(String pattern) {
198        int countStars = pattern.length() - pattern.replace(WILDCARD_CHAR, "").length();
199        if (countStars == 0) {
200          checkArgument(pattern.startsWith("/"), "URL pattern must start with slash '/': %s", pattern);
201          return url -> url.equals(pattern);
202        }
203        checkArgument(countStars == 1, "URL pattern accepts only zero or one wildcard character '*': %s", pattern);
204        if (pattern.charAt(0) == '/') {
205          checkArgument(pattern.endsWith(WILDCARD_CHAR), "URL pattern must end with wildcard character '*': %s", pattern);
206          // remove the ending /* or *
207          String path = pattern.replaceAll("/?\\*", "");
208          return url -> url.startsWith(path);
209        }
210        checkArgument(pattern.startsWith(WILDCARD_CHAR), "URL pattern must start with wildcard character '*': %s", pattern);
211        // remove the leading *
212        String path = pattern.substring(1);
213        return url -> url.endsWith(path);
214      }
215    }
216  }
217}