diff --git a/dotCMS/src/main/java/com/dotcms/analytics/track/AnalyticsTrackWebInterceptor.java b/dotCMS/src/main/java/com/dotcms/analytics/track/AnalyticsTrackWebInterceptor.java new file mode 100644 index 000000000000..64b448b6e290 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/analytics/track/AnalyticsTrackWebInterceptor.java @@ -0,0 +1,105 @@ +package com.dotcms.analytics.track; + +import com.dotcms.filters.interceptor.Result; +import com.dotcms.filters.interceptor.WebInterceptor; +import com.dotcms.jitsu.EventLogSubmitter; +import com.dotcms.util.CollectionsUtils; +import com.dotcms.util.WhiteBlackList; +import com.dotmarketing.util.Config; +import com.dotmarketing.util.Logger; +import com.liferay.util.StringPool; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; + +/** + * Web Interceptor to track analytics + * @author jsanca + */ +public class AnalyticsTrackWebInterceptor implements WebInterceptor { + + private final static Map requestMatchersMap = new ConcurrentHashMap<>(); + + private final EventLogSubmitter submitter; + + /// private static final String[] DEFAULT_BLACKLISTED_PROPS = new String[]{"^/api/*"}; + private static final String[] DEFAULT_BLACKLISTED_PROPS = new String[]{StringPool.BLANK}; + private final WhiteBlackList whiteBlackList = new WhiteBlackList.Builder() + .addWhitePatterns(Config.getStringArrayProperty("ANALYTICS_WHITELISTED_KEYS", + new String[]{StringPool.BLANK})) // allows everything + .addBlackPatterns(CollectionsUtils.concat(Config.getStringArrayProperty( // except this + "ANALYTICS_BLACKLISTED_KEYS", new String[]{}), DEFAULT_BLACKLISTED_PROPS)).build(); + + public AnalyticsTrackWebInterceptor() { + + submitter = new EventLogSubmitter(); + addRequestMatcher( + new PagesAndUrlMapsRequestMatcher(), + new FilesRequestMatcher(), + new RulesRedirectsRequestMatcher(), + new VanitiesRequestMatcher()); + } + + /** + * Add a request matchers + * @param requestMatchers + */ + public static void addRequestMatcher(final RequestMatcher... requestMatchers) { + for (final RequestMatcher matcher : requestMatchers) { + requestMatchersMap.put(matcher.getId(), matcher); + } + } + + /** + * Remove a request matcher by id + * @param requestMatcherId + */ + public static void removeRequestMatcher(final String requestMatcherId) { + + requestMatchersMap.remove(requestMatcherId); + } + + @Override + public Result intercept(final HttpServletRequest request, final HttpServletResponse response) throws IOException { + + if (whiteBlackList.isAllowed(request.getRequestURI())) { + final Optional matcherOpt = this.anyMatcher(request, response, RequestMatcher::runBeforeRequest); + if (matcherOpt.isPresent()) { + + Logger.debug(this, () -> "intercept, Matched: " + matcherOpt.get().getId() + " request: " + request.getRequestURI()); + //fireNextStep(request, response); + } + } + + return Result.NEXT; + } + + @Override + public boolean afterIntercept(final HttpServletRequest request, final HttpServletResponse response) { + + if (whiteBlackList.isAllowed(request.getRequestURI())) { + final Optional matcherOpt = this.anyMatcher(request, response, RequestMatcher::runAfterRequest); + if (matcherOpt.isPresent()) { + + Logger.debug(this, () -> "afterIntercept, Matched: " + matcherOpt.get().getId() + " request: " + request.getRequestURI()); + //fireNextStep(request, response); + } + } + + return true; + } + + private Optional anyMatcher(final HttpServletRequest request, final HttpServletResponse response, Predicate filterRequest) { + + return requestMatchersMap.values().stream() + .filter(filterRequest) + .filter(matcher -> matcher.match(request, response)) + .findFirst(); + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/analytics/track/FilesRequestMatcher.java b/dotCMS/src/main/java/com/dotcms/analytics/track/FilesRequestMatcher.java new file mode 100644 index 000000000000..d5067670615d --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/analytics/track/FilesRequestMatcher.java @@ -0,0 +1,48 @@ +package com.dotcms.analytics.track; + +import com.dotcms.visitor.filter.characteristics.Character; +import com.dotcms.visitor.filter.characteristics.CharacterWebAPI; +import com.dotmarketing.business.web.WebAPILocator; +import com.dotmarketing.filters.CMSFilter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Objects; + +/** + * Matcher for pages or files + * @author jsanca + */ +public class FilesRequestMatcher implements RequestMatcher { + + private final CharacterWebAPI characterWebAPI; + + public FilesRequestMatcher() { + this(WebAPILocator.getCharacterWebAPI()); + } + + public FilesRequestMatcher(final CharacterWebAPI characterWebAPI) { + this.characterWebAPI = characterWebAPI; + } + + @Override + public boolean runBeforeRequest() { + return true; + } + + @Override + public boolean match(final HttpServletRequest request, final HttpServletResponse response) { + + final Character character = this.characterWebAPI.getOrCreateCharacter(request, response); + if (Objects.nonNull(character)) { + + final CMSFilter.IAm iAm = (CMSFilter.IAm) character.getMap(). + getOrDefault("iAm", CMSFilter.IAm.NOTHING_IN_THE_CMS); + + // should we have a fallback when nothing is returned??? + return iAm == CMSFilter.IAm.FILE; + } + + return false; + } +} diff --git a/dotCMS/src/main/java/com/dotcms/analytics/track/PagesAndUrlMapsRequestMatcher.java b/dotCMS/src/main/java/com/dotcms/analytics/track/PagesAndUrlMapsRequestMatcher.java new file mode 100644 index 000000000000..df1e24b5c64d --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/analytics/track/PagesAndUrlMapsRequestMatcher.java @@ -0,0 +1,48 @@ +package com.dotcms.analytics.track; + +import com.dotcms.visitor.filter.characteristics.Character; +import com.dotcms.visitor.filter.characteristics.CharacterWebAPI; +import com.dotmarketing.business.web.WebAPILocator; +import com.dotmarketing.filters.CMSFilter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Objects; + +/** + * Matcher for pages or files + * @author jsanca + */ +public class PagesAndUrlMapsRequestMatcher implements RequestMatcher { + + private final CharacterWebAPI characterWebAPI; + + public PagesAndUrlMapsRequestMatcher() { + this(WebAPILocator.getCharacterWebAPI()); + } + + public PagesAndUrlMapsRequestMatcher(final CharacterWebAPI characterWebAPI) { + this.characterWebAPI = characterWebAPI; + } + + @Override + public boolean runBeforeRequest() { + return true; + } + + @Override + public boolean match(final HttpServletRequest request, final HttpServletResponse response) { + + final Character character = this.characterWebAPI.getOrCreateCharacter(request, response); + if (Objects.nonNull(character)) { + + final CMSFilter.IAm iAm = (CMSFilter.IAm) character.getMap(). + getOrDefault("iAm", CMSFilter.IAm.NOTHING_IN_THE_CMS); + + // should we have a fallback when nothing is returned??? + return iAm == CMSFilter.IAm.PAGE; // this captures also url maps + } + + return false; + } +} diff --git a/dotCMS/src/main/java/com/dotcms/analytics/track/RequestMatcher.java b/dotCMS/src/main/java/com/dotcms/analytics/track/RequestMatcher.java new file mode 100644 index 000000000000..0cbd764d1c3a --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/analytics/track/RequestMatcher.java @@ -0,0 +1,126 @@ +package com.dotcms.analytics.track; + +import com.dotmarketing.util.Config; +import com.dotmarketing.util.RegEX; +import io.vavr.control.Try; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.Objects; +import java.util.Set; + +/** + * Matcher to include the tracking for analytics of some request. + * + * @author jsanca + */ +public interface RequestMatcher { + + + String CHARSET = Try.of(()->Config.getStringProperty("CHARSET", "UTF-8")).getOrElse("UTF-8"); + + /** + * Return true if match the request with the patterns and methods + * @param request {@link HttpServletRequest} + * @param response {@link HttpServletResponse} + * @return boolean true if the request match the patterns and methods + */ + default boolean match(final HttpServletRequest request, final HttpServletResponse response) { + + final Set patterns = getMatcherPatterns(); + final Set methods = getAllowedMethods(); + return Objects.nonNull(patterns) && !patterns.isEmpty() && + Objects.nonNull(methods) && !methods.isEmpty() && + isAllowedMethod (methods, request.getMethod()) && + match(request, response, patterns); + } + + /** + * Match the request with the patterns + * @param request {@link HttpServletRequest} + * @param response {@link HttpServletResponse} + * @param patterns Set of patterns + * @return boolean true if any of the patterns match the request + */ + default boolean match(final HttpServletRequest request, final HttpServletResponse response, final Set patterns) { + + final String requestURI = request.getRequestURI(); + return patterns.stream().anyMatch(pattern -> match(requestURI, pattern)); + } + + /** + * Means the matcher should run before request + * @return boolean + */ + default boolean runBeforeRequest() { + return false; + } + + /** + * Means the matcher should run after request + * @return boolean + */ + default boolean runAfterRequest() { + return false; + } + /** + * Match the request URI with the pattern + * @param requestURI String + * @param pattern String + * @return boolean true if the pattern match the request URI + */ + default boolean match (final String requestURI, final String pattern) { + + String uftUri = null; + + try { + + uftUri = URLDecoder.decode(requestURI, CHARSET); + } catch (UnsupportedEncodingException e) { + + uftUri = requestURI; + } + + return RegEX.containsCaseInsensitive(uftUri, pattern.trim()); + } // match. + + /** + * Determinate if the method is allowed + * @param methods Set of methods + * @param method String current request method + * @return boolean true if the method is allowed + */ + default boolean isAllowedMethod(final Set methods, final String method) { + + return methods.contains(method); + } + + /** + * Returns a set of patterns for the matcher + * @return Set by default empty + */ + default Set getMatcherPatterns() { + + return Set.of(); + } + + /** + * Returns the request methods allowed for this matcher. + * @return Set by default empty + */ + default Set getAllowedMethods() { + + return Set.of(); + } + + /** + * Return an id for the Matcher, by default returns the class name. + * @return + */ + default String getId() { + + return this.getClass().getName(); + } +} diff --git a/dotCMS/src/main/java/com/dotcms/analytics/track/RulesRedirectsRequestMatcher.java b/dotCMS/src/main/java/com/dotcms/analytics/track/RulesRedirectsRequestMatcher.java new file mode 100644 index 000000000000..a32c4badccfe --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/analytics/track/RulesRedirectsRequestMatcher.java @@ -0,0 +1,24 @@ +package com.dotcms.analytics.track; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Objects; + +/** + * Matcher for vanity urls and rules redirect + * @author jsanca + */ +public class RulesRedirectsRequestMatcher implements RequestMatcher { + + @Override + public boolean runAfterRequest() { + return true; + } + + @Override + public boolean match(final HttpServletRequest request, final HttpServletResponse response) { + + final String ruleRedirect = response.getHeader("X-DOT-SendRedirectRuleAction"); + return Objects.nonNull(ruleRedirect) && "true".equals(ruleRedirect); + } +} diff --git a/dotCMS/src/main/java/com/dotcms/analytics/track/VanitiesRequestMatcher.java b/dotCMS/src/main/java/com/dotcms/analytics/track/VanitiesRequestMatcher.java new file mode 100644 index 000000000000..433d8a2853da --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/analytics/track/VanitiesRequestMatcher.java @@ -0,0 +1,27 @@ +package com.dotcms.analytics.track; + +import com.dotmarketing.filters.Constants; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Objects; + +/** + * Matcher for vanity urls and rules redirect + * @author jsanca + */ +public class VanitiesRequestMatcher implements RequestMatcher { + + @Override + public boolean runAfterRequest() { + return true; + } + + @Override + public boolean match(final HttpServletRequest request, final HttpServletResponse response) { + + final Object vanityHasRun = request.getAttribute(Constants.VANITY_URL_OBJECT); + + return Objects.nonNull(vanityHasRun); + } +} diff --git a/dotCMS/src/main/java/com/dotcms/jitsu/EventLogSubmitter.java b/dotCMS/src/main/java/com/dotcms/jitsu/EventLogSubmitter.java index fc9025bca514..1b6e27335634 100644 --- a/dotCMS/src/main/java/com/dotcms/jitsu/EventLogSubmitter.java +++ b/dotCMS/src/main/java/com/dotcms/jitsu/EventLogSubmitter.java @@ -19,7 +19,7 @@ public class EventLogSubmitter { private final SubmitterConfig submitterConfig; - EventLogSubmitter() { + public EventLogSubmitter() { submitterConfig = new DotConcurrentFactory.SubmitterConfigBuilder() .poolSize(Config.getIntProperty("EVENT_LOG_POSTING_THREADS", 8)) .maxPoolSize(Config.getIntProperty("EVENT_LOG_POSTING_THREADS_MAX", 16)) @@ -29,7 +29,7 @@ public class EventLogSubmitter { .build(); } - void logEvent(final Host host, final EventsPayload eventPayload) { + public void logEvent(final Host host, final EventsPayload eventPayload) { DotConcurrentFactory .getInstance() .getSubmitter("event-log-posting", submitterConfig) diff --git a/dotCMS/src/main/java/com/dotcms/mock/request/DotCMSMockRequest.java b/dotCMS/src/main/java/com/dotcms/mock/request/DotCMSMockRequest.java index 8845dc716ad6..57a62022e906 100644 --- a/dotCMS/src/main/java/com/dotcms/mock/request/DotCMSMockRequest.java +++ b/dotCMS/src/main/java/com/dotcms/mock/request/DotCMSMockRequest.java @@ -27,6 +27,7 @@ import javax.servlet.http.HttpSession; import javax.servlet.http.HttpUpgradeHandler; import javax.servlet.http.Part; +import javax.ws.rs.HttpMethod; public class DotCMSMockRequest implements HttpServletRequest { @@ -41,6 +42,8 @@ public class DotCMSMockRequest implements HttpServletRequest { private String servletPath; final private Map attributes = new HashMap<>(); protected byte[] content; + private String method = HttpMethod.GET; + @Override public String getRequestURI() { @@ -168,9 +171,12 @@ public int getIntHeader(String s) { return 0; } + public void setMethod(String method) { + this.method = method; + } @Override public String getMethod() { - return null; + return method; } @Override @@ -488,4 +494,4 @@ public void setContent(byte[] content) { public void setContent(String content) { this.content = content.getBytes(); } -} \ No newline at end of file +} diff --git a/dotCMS/src/main/java/com/dotcms/mock/response/DotCMSMockResponse.java b/dotCMS/src/main/java/com/dotcms/mock/response/DotCMSMockResponse.java index b79d9298999c..69387c855e9b 100644 --- a/dotCMS/src/main/java/com/dotcms/mock/response/DotCMSMockResponse.java +++ b/dotCMS/src/main/java/com/dotcms/mock/response/DotCMSMockResponse.java @@ -1,5 +1,6 @@ package com.dotcms.mock.response; +import com.dotcms.business.SystemCache; import com.dotcms.ema.proxy.MockPrintWriter; import com.dotcms.repackage.org.directwebremoting.util.FakeHttpServletResponse; import java.io.IOException; @@ -22,6 +23,7 @@ public class DotCMSMockResponse implements HttpServletResponse { private ServletOutputStream outputStream; private List cookies = new ArrayList<>(); + private Map headersMap = new HashMap<>(); @Override public void addCookie(final Cookie cookie) { @@ -90,6 +92,7 @@ public void setHeader(String s, String s1) { @Override public void addHeader(String s, String s1) { + headersMap.put(s, s1); } @Override @@ -119,7 +122,7 @@ public int getStatus() { @Override public String getHeader(String s) { - return null; + return headersMap.get(s); } @Override diff --git a/dotCMS/src/main/java/com/dotcms/visitor/filter/characteristics/BaseCharacter.java b/dotCMS/src/main/java/com/dotcms/visitor/filter/characteristics/BaseCharacter.java index 6a153d957c2f..30d148f468b7 100644 --- a/dotCMS/src/main/java/com/dotcms/visitor/filter/characteristics/BaseCharacter.java +++ b/dotCMS/src/main/java/com/dotcms/visitor/filter/characteristics/BaseCharacter.java @@ -59,7 +59,7 @@ private BaseCharacter(final HttpServletRequest request, final HttpServletRespons final Optional content = Optional.ofNullable((String) request.getAttribute(WebKeys.WIKI_CONTENTLET)); final Language lang = WebAPILocator.getLanguageWebAPI().getLanguage(request); IAm iAm = resolveResourceType(uri, getHostNoThrow(request), lang.getId()); - final long pageProcessingTime = (Long) request.getAttribute(VisitorFilter.DOTPAGE_PROCESSING_TIME); + final Long pageProcessingTime = (Long) request.getAttribute(VisitorFilter.DOTPAGE_PROCESSING_TIME); myMap.get().put("id", UUID.randomUUID().toString()); myMap.get().put("status", response.getStatus()); myMap.get().put("iAm", iAm); diff --git a/dotCMS/src/main/java/com/dotcms/visitor/filter/characteristics/CharacterWebAPI.java b/dotCMS/src/main/java/com/dotcms/visitor/filter/characteristics/CharacterWebAPI.java new file mode 100644 index 000000000000..c0bb069f278d --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/visitor/filter/characteristics/CharacterWebAPI.java @@ -0,0 +1,30 @@ +package com.dotcms.visitor.filter.characteristics; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Optional; + +/** + * Character Web API + * @author jsanca + */ +public interface CharacterWebAPI { + + String DOT_CHARACTER = "dotCharacter"; + + /** + * Get or create a character + * @param request HttpServletRequest + * @param response HttpServletResponse + * @return Character + */ + Character getOrCreateCharacter(final HttpServletRequest request, final HttpServletResponse response); + + /** + * Get a character if exist + * @param request HttpServletRequest + * @param response HttpServletResponse + * @return Character + */ + Optional getCharacterIfExist(final HttpServletRequest request, final HttpServletResponse response); +} diff --git a/dotCMS/src/main/java/com/dotcms/visitor/filter/characteristics/CharacterWebAPIImpl.java b/dotCMS/src/main/java/com/dotcms/visitor/filter/characteristics/CharacterWebAPIImpl.java new file mode 100644 index 000000000000..4fcedb0237f4 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/visitor/filter/characteristics/CharacterWebAPIImpl.java @@ -0,0 +1,42 @@ +package com.dotcms.visitor.filter.characteristics; + +import com.dotcms.visitor.filter.logger.VisitorLogger; +import io.vavr.control.Try; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Objects; +import java.util.Optional; + +/** + * Character Web API + * @author jsanca + */ +public class CharacterWebAPIImpl implements CharacterWebAPI { + + @Override + public Character getOrCreateCharacter(final HttpServletRequest request, final HttpServletResponse response) { + + final Optional characterOptional = getCharacterIfExist(request, response); + + if (characterOptional.isPresent()) { + return characterOptional.get(); + } + + final Character character = Try.of(()->VisitorLogger.createCharacter(request, response)).getOrNull(); + + if (Objects.nonNull(character)) { + + request.setAttribute(DOT_CHARACTER, character); + } + + return character; + } + + @Override + public Optional getCharacterIfExist(final HttpServletRequest request, final HttpServletResponse response) { + + final Character character = (Character) request.getAttribute(DOT_CHARACTER); + return Optional.ofNullable(character); + } +} diff --git a/dotCMS/src/main/java/com/dotcms/visitor/filter/logger/VisitorLogger.java b/dotCMS/src/main/java/com/dotcms/visitor/filter/logger/VisitorLogger.java index 316cdf549969..d36297f7f157 100644 --- a/dotCMS/src/main/java/com/dotcms/visitor/filter/logger/VisitorLogger.java +++ b/dotCMS/src/main/java/com/dotcms/visitor/filter/logger/VisitorLogger.java @@ -4,6 +4,7 @@ import com.dotcms.visitor.business.VisitorAPIImpl; import com.dotcms.visitor.filter.characteristics.*; +import com.dotcms.visitor.filter.characteristics.Character; import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.logConsole.model.LogMapper; @@ -118,24 +119,42 @@ public static List> removeConstructor( } private static void logInternal(final HttpServletRequest request, final HttpServletResponse response) - throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, + throws SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { - AbstractCharacter base = new BaseCharacter(request, response); try { - for (Constructor con : constructors) { - base = con.newInstance(base); - } - - for (Constructor con : customConstructors) { - base = con.newInstance(base); - } + final Character base = createCharacter(request, response); doLog(mapper().writeValueAsString(base.getMap())); } catch (JsonProcessingException e) { throw new DotRuntimeException(e); } } + /** + * Method that creates a character + * @param request + * @param response + * @return + * @throws InvocationTargetException + * @throws InstantiationException + * @throws IllegalAccessException + */ + public static Character createCharacter(final HttpServletRequest request, + final HttpServletResponse response) + throws InvocationTargetException, InstantiationException, IllegalAccessException { + + AbstractCharacter base = new BaseCharacter(request, response); + for (final Constructor con : constructors) { + base = con.newInstance(base); + } + + for (final Constructor con : customConstructors) { + base = con.newInstance(base); + } + + return base; + } + private static void doLog(String message) { Logger.info(VisitorLogger.class, message); } diff --git a/dotCMS/src/main/java/com/dotmarketing/business/web/WebAPILocator.java b/dotCMS/src/main/java/com/dotmarketing/business/web/WebAPILocator.java index fdcb27d91533..1771c7a9084a 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/web/WebAPILocator.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/web/WebAPILocator.java @@ -9,6 +9,8 @@ import com.dotcms.experiments.business.web.ExperimentWebAPIImpl; import com.dotcms.variant.business.web.VariantWebAPI; import com.dotcms.variant.business.web.VariantWebAPIImpl; +import com.dotcms.visitor.filter.characteristics.CharacterWebAPI; +import com.dotcms.visitor.filter.characteristics.CharacterWebAPIImpl; import com.dotmarketing.business.Locator; import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.portlets.contentlet.business.web.ContentletWebAPI; @@ -73,6 +75,10 @@ public static PreRenderSEOWebAPI getPreRenderSEOWebAPI() { return (PreRenderSEOWebAPI)getInstance(WebAPIIndex.PRERENDER_API); } + public static CharacterWebAPI getCharacterWebAPI() { + return (CharacterWebAPI)getInstance(WebAPIIndex.CHARACTER_API); + } + private static Object getInstance(WebAPIIndex index) { if(instance == null){ @@ -113,7 +119,8 @@ enum WebAPIIndex PERMISSION_WEB_API, HOST_WEB_API, PERSONALIZATION_WEB_API, - PRERENDER_API; + PRERENDER_API, + CHARACTER_API; Object create() { switch(this) { @@ -140,6 +147,9 @@ Object create() { case EXPERIMENT_WEB_API: return new ExperimentWebAPIImpl(); + + case CHARACTER_API: + return new CharacterWebAPIImpl(); } throw new AssertionError("Unknown API index: " + this); } diff --git a/dotCMS/src/main/java/com/dotmarketing/filters/InterceptorFilter.java b/dotCMS/src/main/java/com/dotmarketing/filters/InterceptorFilter.java index 23dfd814badd..87d2f78245ac 100644 --- a/dotCMS/src/main/java/com/dotmarketing/filters/InterceptorFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/filters/InterceptorFilter.java @@ -1,5 +1,6 @@ package com.dotmarketing.filters; +import com.dotcms.analytics.track.AnalyticsTrackWebInterceptor; import com.dotcms.ema.EMAWebInterceptor; import com.dotcms.filters.interceptor.AbstractWebInterceptorSupportFilter; import com.dotcms.filters.interceptor.WebInterceptorDelegate; @@ -39,6 +40,7 @@ private void addInterceptors(final FilterConfig config) { delegate.add(new ResponseMetaDataWebInterceptor()); delegate.add(new EventLogWebInterceptor()); delegate.add(new CurrentVariantWebInterceptor()); + delegate.add(new AnalyticsTrackWebInterceptor()); } // addInterceptors. } // E:O:F:InterceptorFilter. diff --git a/dotcms-integration/src/test/java/com/dotcms/MainSuite1a.java b/dotcms-integration/src/test/java/com/dotcms/MainSuite1a.java index e2cc7ed38e3e..e9758e583ff6 100644 --- a/dotcms-integration/src/test/java/com/dotcms/MainSuite1a.java +++ b/dotcms-integration/src/test/java/com/dotcms/MainSuite1a.java @@ -1,6 +1,7 @@ package com.dotcms; import com.dotcms.ai.workflow.OpenAIGenerateImageActionletTest; +import com.dotcms.analytics.track.RequestMatcherTest; import com.dotcms.content.elasticsearch.business.ESContentletAPIImplTest; import com.dotcms.contenttype.business.SiteAndFolderResolverImplTest; import com.dotcms.enterprise.publishing.remote.PushPublishBundleGeneratorTest; @@ -105,7 +106,8 @@ JsEngineTest.class, Task240306MigrateLegacyLanguageVariablesTest.class, EmailActionletTest.class, - OpenAIGenerateImageActionletTest.class + OpenAIGenerateImageActionletTest.class, + RequestMatcherTest.class }) public class MainSuite1a { diff --git a/dotcms-integration/src/test/java/com/dotcms/analytics/track/RequestMatcherTest.java b/dotcms-integration/src/test/java/com/dotcms/analytics/track/RequestMatcherTest.java new file mode 100644 index 000000000000..cce2d358a8af --- /dev/null +++ b/dotcms-integration/src/test/java/com/dotcms/analytics/track/RequestMatcherTest.java @@ -0,0 +1,277 @@ +package com.dotcms.analytics.track; + +import com.dotcms.mock.request.DotCMSMockRequest; +import com.dotcms.mock.request.FakeHttpRequest; +import com.dotcms.mock.response.DotCMSMockResponse; +import com.dotcms.mock.response.MockHttpResponse; +import com.dotcms.util.IntegrationTestInitService; +import com.dotcms.vanityurl.model.DefaultVanityUrl; +import com.dotcms.visitor.filter.characteristics.Character; +import com.dotcms.visitor.filter.characteristics.CharacterWebAPI; +import com.dotmarketing.filters.CMSFilter; +import com.dotmarketing.filters.Constants; +import org.junit.BeforeClass; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.HttpMethod; +import java.io.Serializable; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Test class for RequestMatcher + */ +public class RequestMatcherTest { + + @BeforeClass + public static void beforeClass() throws Exception { + + IntegrationTestInitService.getInstance().init(); + } + + /** + * Method to test: RequestMatcher.match(HttpServletRequest request) + * Given Scenario: Will use a default implementation of the method (without implementing anything) + * ExpectedResult: Any of the matches wont work, so the method will return false + */ + @Test + public void test_default_implementation() throws Exception { + + final RequestMatcher requestMatcher = new RequestMatcher() { + // empty implementation + }; + final FakeHttpRequest request = new FakeHttpRequest("localhost", "/test"); + final boolean result = requestMatcher.match(request.request(), new MockHttpResponse().response()); + assertFalse(result); + } + + /** + * Method to test: RequestMatcher.match(HttpServletRequest request) + * Given Scenario: Creates a matcher for the exact uri path and the GET method + * ExpectedResult: Since both criteria are met, the method will return true + */ + @Test + public void test_get_method_with_valid_exact_match() throws Exception { + + final String url = "/test"; + final RequestMatcher requestMatcher = new RequestMatcher() { + + @Override + public boolean runBeforeRequest() { + return true; + } + + @Override + public Set getMatcherPatterns() { + return Set.of(url); + } + + @Override + public Set getAllowedMethods() { + return Set.of(HttpMethod.GET); + } + }; + + final DotCMSMockRequest mockReq = new DotCMSMockRequest(); + + mockReq.setRequestURI("/api/v1/test"); + mockReq.setRequestURL(new StringBuffer("http://localhost" + url)); + mockReq.setServerName("http://localhost"); + mockReq.setMethod(HttpMethod.GET); + final boolean result = requestMatcher.match(mockReq, new MockHttpResponse().response()); + assertTrue(result); + } + + /** + * Method to test: RequestMatcher.match(HttpServletRequest request) + * Given Scenario: Creates a matcher for the wildcard and the GET/post method + * ExpectedResult: Since both criteria are met, the method will return true + */ + @Test + public void test_get_and_post_method_with_wildcard_match() throws Exception { + + final String url = "/test*"; + final RequestMatcher requestMatcher = new RequestMatcher() { + + @Override + public boolean runBeforeRequest() { + return true; + } + + @Override + public Set getMatcherPatterns() { + return Set.of(url); + } + + @Override + public Set getAllowedMethods() { + return Set.of(HttpMethod.GET, HttpMethod.POST); + } + }; + + DotCMSMockRequest mockReq = new DotCMSMockRequest(); + + mockReq.setRequestURI("/api/v1/test"); + mockReq.setRequestURL(new StringBuffer("http://localhost/api/v1/test")); + mockReq.setServerName("http://localhost"); + mockReq.setMethod(HttpMethod.GET); + boolean result = requestMatcher.match(mockReq, new MockHttpResponse().response()); + assertTrue(result); + + mockReq = new DotCMSMockRequest(); + + mockReq.setRequestURI("/dA/test"); + mockReq.setRequestURL(new StringBuffer("http://localhost/dA/test")); + mockReq.setServerName("http://localhost"); + mockReq.setMethod(HttpMethod.POST); + result = requestMatcher.match(mockReq, new MockHttpResponse().response()); + assertTrue(result); + } + + /** + * Method to test: RequestMatcher.match(HttpServletRequest request) + * Given Scenario: Creates a matcher to identify pages + * ExpectedResult: Since sending a page will return true + */ + @Test + public void test_get_method_with_pages_matcher() throws Exception { + + final CharacterWebAPI characterWebAPI = new CharacterWebAPI() { + @Override + public Character getOrCreateCharacter(HttpServletRequest request, HttpServletResponse response) { + + + return new Character() { + @Override + public Map getMap() { + return Map.of("iAm", CMSFilter.IAm.PAGE); + } + + @Override + public void clearMap() { + + } + }; + } + + @Override + public Optional getCharacterIfExist(HttpServletRequest request, HttpServletResponse response) { + return Optional.empty(); + } + }; + + final RequestMatcher requestMatcher = new PagesAndUrlMapsRequestMatcher(characterWebAPI); + + final DotCMSMockRequest mockReq = new DotCMSMockRequest(); + + mockReq.setRequestURI("/site/test"); + mockReq.setRequestURL(new StringBuffer("http://localhost/site/test")); + mockReq.setServerName("http://localhost"); + mockReq.setMethod(HttpMethod.GET); + final boolean result = requestMatcher.match(mockReq, new MockHttpResponse().response()); + assertTrue(result); + } + + /** + * Method to test: RequestMatcher.match(HttpServletRequest request) + * Given Scenario: Creates a matcher to identify files + * ExpectedResult: Since sending a file will return true + */ + @Test + public void test_get_method_with_file_matcher() throws Exception { + + final CharacterWebAPI characterWebAPI = new CharacterWebAPI() { + @Override + public Character getOrCreateCharacter(HttpServletRequest request, HttpServletResponse response) { + + + return new Character() { + @Override + public Map getMap() { + return Map.of("iAm", CMSFilter.IAm.FILE); + } + + @Override + public void clearMap() { + + } + }; + } + + @Override + public Optional getCharacterIfExist(HttpServletRequest request, HttpServletResponse response) { + return Optional.empty(); + } + }; + + final RequestMatcher requestMatcher = new FilesRequestMatcher(characterWebAPI); + + final DotCMSMockRequest mockReq = new DotCMSMockRequest(); + + mockReq.setRequestURI("/dA/test"); + mockReq.setRequestURL(new StringBuffer("http://localhost/dA/test")); + mockReq.setServerName("http://localhost"); + mockReq.setMethod(HttpMethod.GET); + final boolean result = requestMatcher.match(mockReq, new MockHttpResponse().response()); + assertTrue(result); + } + + /** + * Method to test: RequestMatcher.match(HttpServletRequest request) + * Given Scenario: Creates a matcher to identify a rules redirection + * ExpectedResult: Since sending a rules redirection will return true + */ + @Test + public void test_get_method_with_rules_matcher() throws Exception { + + final RequestMatcher requestMatcher = new RulesRedirectsRequestMatcher(); + + final DotCMSMockRequest mockReq = new DotCMSMockRequest(); + + mockReq.setRequestURI("/dA/test"); + mockReq.setRequestURL(new StringBuffer("http://localhost/dA/test")); + mockReq.setServerName("http://localhost"); + mockReq.setMethod(HttpMethod.GET); + + + final DotCMSMockResponse response = new DotCMSMockResponse(); + + response.addHeader("X-DOT-SendRedirectRuleAction", "true"); + final boolean result = requestMatcher.match(mockReq, response); + assertTrue(result); + } + + /** + * Method to test: RequestMatcher.match(HttpServletRequest request) + * Given Scenario: Creates a matcher to identify a vanities redirection + * ExpectedResult: Since sending a vanities redirection will return true + */ + @Test + public void test_get_method_with_vanities_matcher() throws Exception { + + final RequestMatcher requestMatcher = new VanitiesRequestMatcher(); + + final DotCMSMockRequest mockReq = new DotCMSMockRequest(); + + mockReq.setRequestURI("/dA/test"); + mockReq.setRequestURL(new StringBuffer("http://localhost/dA/test")); + mockReq.setServerName("http://localhost"); + mockReq.setMethod(HttpMethod.GET); + final DefaultVanityUrl defaultVanityUrl = new DefaultVanityUrl(); + defaultVanityUrl.setForwardTo("/test"); + mockReq.setAttribute(Constants.VANITY_URL_OBJECT, defaultVanityUrl); + + + final DotCMSMockResponse response = new DotCMSMockResponse(); + + final boolean result = requestMatcher.match(mockReq, response); + assertTrue(result); + } + +}