package com.psddev.cms.db; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.net.URLConnection; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import com.google.common.base.Preconditions; import com.psddev.cms.view.ViewTemplateLoader; import com.psddev.dari.util.CompactMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.psddev.cms.tool.AuthenticationFilter; import com.psddev.cms.tool.CmsTool; import com.psddev.cms.tool.RemoteWidgetFilter; import com.psddev.cms.tool.ToolPageContext; import com.psddev.cms.view.AbstractViewCreator; import com.psddev.cms.view.EmbedEntryView; import com.psddev.cms.view.JsonViewRenderer; import com.psddev.cms.view.PageEntryView; import com.psddev.cms.view.PageViewClass; import com.psddev.cms.view.PreviewEntryView; import com.psddev.cms.view.ViewBinding; import com.psddev.cms.view.ViewCreator; import com.psddev.cms.view.ViewMapping; import com.psddev.cms.view.ViewModel; import com.psddev.cms.view.ViewModelCreator; import com.psddev.cms.view.ViewOutput; import com.psddev.cms.view.ViewRenderer; import com.psddev.cms.view.ViewRequest; import com.psddev.cms.view.servlet.ServletViewTemplateLoader; import com.psddev.cms.view.servlet.ServletViewModelCreator; import com.psddev.cms.view.servlet.ServletViewRequestAnnotationProcessor; import com.psddev.cms.view.servlet.ServletViewRequestAnnotationProcessorClass; import com.psddev.cms.view.ViewResponse; import com.psddev.dari.db.Application; import com.psddev.dari.db.ApplicationFilter; import com.psddev.dari.db.Database; import com.psddev.dari.db.ObjectType; import com.psddev.dari.db.Query; import com.psddev.dari.db.Record; import com.psddev.dari.db.Recordable; import com.psddev.dari.db.State; import com.psddev.dari.util.AbstractFilter; import com.psddev.dari.util.Converter; import com.psddev.dari.util.ErrorUtils; import com.psddev.dari.util.HtmlWriter; import com.psddev.dari.util.JspBufferFilter; import com.psddev.dari.util.JspUtils; import com.psddev.dari.util.ObjectUtils; import com.psddev.dari.util.PageContextFilter; import com.psddev.dari.util.Profiler; import com.psddev.dari.util.PullThroughCache; import com.psddev.dari.util.Settings; import com.psddev.dari.util.StorageItemFilter; import com.psddev.dari.util.StringUtils; import com.psddev.dari.util.TypeDefinition; public class PageFilter extends AbstractFilter { /** @deprecated No replacement. */ @Deprecated public static final String WIREFRAME_PARAMETER = "_wireframe"; private static final Logger LOGGER = LoggerFactory.getLogger(PageFilter.class); private static final String PARAMETER_PREFIX = "_cms.db."; public static final String DEBUG_PARAMETER = PARAMETER_PREFIX + "debug"; /** * @deprecated No replacement. */ @Deprecated public static final String OVERLAY_PARAMETER = PARAMETER_PREFIX + "overlay"; public static final String PREVIEW_DATA_PARAMETER = PARAMETER_PREFIX + "previewData"; public static final String PREVIEW_ID_PARAMETER = PARAMETER_PREFIX + "previewId"; public static final String PREVIEW_SITE_ID_PARAMETER = "_previewSiteId"; public static final String PREVIEW_TYPE_ID_PARAMETER = PARAMETER_PREFIX + "previewTypeId"; public static final String PREVIEW_OBJECT_PARAMETER = "_previewObject"; private static final String ATTRIBUTE_PREFIX = PageFilter.class.getName(); private static final String FIXED_PATH_ATTRIBUTE = ATTRIBUTE_PREFIX + ".fixedPath"; private static final String NEW_REQUEST_ATTRIBUTE = ATTRIBUTE_PREFIX + ".newRequest"; private static final String PATH_ATTRIBUTE = ATTRIBUTE_PREFIX + ".path"; private static final String PATH_MATCHES_ATTRIBUTE = ATTRIBUTE_PREFIX + ".matches"; private static final String PREVIEW_ATTRIBUTE = ".preview"; private static final String PERSISTENT_PREVIEW_ATTRIBUTE = ".persistentPreview"; private static final String VIEW_TEMPLATE_LOADER_ATTRIBUTE = ATTRIBUTE_PREFIX + ".viewTemplateLoader"; public static final String ABORTED_ATTRIBUTE = ATTRIBUTE_PREFIX + ".aborted"; public static final String CURRENT_SECTION_ATTRIBUTE = ATTRIBUTE_PREFIX + ".currentSection"; public static final String MAIN_OBJECT_ATTRIBUTE = ATTRIBUTE_PREFIX + ".mainObject"; public static final String MAIN_OBJECT_CHECKED_ATTRIBUTE = ATTRIBUTE_PREFIX + ".mainObjectChecked"; private static final String OBJECTS_ATTRIBUTE = ATTRIBUTE_PREFIX + ".objects"; public static final String PAGE_ATTRIBUTE = ATTRIBUTE_PREFIX + ".page"; public static final String PAGE_CHECKED_ATTRIBUTE = ATTRIBUTE_PREFIX + ".pageChecked"; public static final String PARENT_SECTIONS_ATTRIBUTE = ATTRIBUTE_PREFIX + ".parentSections"; public static final String PROFILE_ATTRIBUTE = ATTRIBUTE_PREFIX + ".profile"; public static final String PROFILE_CHECKED_ATTRIBUTE = ATTRIBUTE_PREFIX + ".profileChecked"; public static final String RENDERED_OBJECTS_ATTRIBUTE = ATTRIBUTE_PREFIX + ".renderedObjects"; public static final String SITE_ATTRIBUTE = ATTRIBUTE_PREFIX + ".site"; public static final String SITE_CHECKED_ATTRIBUTE = ATTRIBUTE_PREFIX + ".siteChecked"; public static final String SUBSTITUTIONS_ATTRIBUTE = ATTRIBUTE_PREFIX + ".substitutions"; public static final String VIEW_TYPE_ATTRIBUTE = ATTRIBUTE_PREFIX + ".viewType"; private static final String ENTRY_VIEW_CLASS_ATTRIBUTE = ATTRIBUTE_PREFIX + ".entryViewClass"; public static final String MAIN_OBJECT_RENDERER_CONTEXT = "_main"; public static final String EMBED_OBJECT_RENDERER_CONTEXT = "_embed"; /** * @deprecated Use {@linkplain PageEntryView} instead. */ @Deprecated public static final String PAGE_VIEW_TYPE = "cms.page"; /** * @deprecated Use {@linkplain PreviewEntryView} instead. */ @Deprecated public static final String PREVIEW_VIEW_TYPE = "cms.preview"; /** * @deprecated Use {@linkplain EmbedEntryView} instead. */ @Deprecated public static final String EMBED_VIEW_TYPE = "cms.embed"; public static final String VIEW_TYPE_PARAMETER = "_viewType"; private boolean poweredBy; /** * Returns {@code true} if rendering the given {@code request} has * been aborted. */ public static boolean isAborted(HttpServletRequest request) { return Boolean.TRUE.equals(request.getAttribute(ABORTED_ATTRIBUTE)); } /** Aborts rendering the given {@code request}. */ public static void abort(HttpServletRequest request) { request.setAttribute(ABORTED_ATTRIBUTE, Boolean.TRUE); } /** * Returns the section currently being rendered in the given * {@code request}. */ public static Section getCurrentSection(HttpServletRequest request) { return (Section) request.getAttribute(CURRENT_SECTION_ATTRIBUTE); } /** * Sets the section currently being rendered in the given * {@code request}. */ protected static void setCurrentSection(HttpServletRequest request, Section section) { request.setAttribute(CURRENT_SECTION_ATTRIBUTE, section); request.setAttribute("section", section); } /** * Returns an unmodifiable list of all the parent sections used to render * the given {@code request} so far. */ public static List<Section> getParentSections(HttpServletRequest request) { @SuppressWarnings("unchecked") List<Section> parents = (List<Section>) request.getAttribute(PARENT_SECTIONS_ATTRIBUTE); return parents != null ? Collections.unmodifiableList(parents) : Collections.<Section>emptyList(); } /** * Adds the given {@code section} to the list of sections used to render * the given {@code request} so far. */ protected static void addParentSection(HttpServletRequest request, Section section) { @SuppressWarnings("unchecked") List<Section> parents = (List<Section>) request.getAttribute(PARENT_SECTIONS_ATTRIBUTE); if (parents == null) { parents = new ArrayList<Section>(); request.setAttribute(PARENT_SECTIONS_ATTRIBUTE, parents); } parents.add(section); @SuppressWarnings("unchecked") Map<String, Boolean> isInside = (Map<String, Boolean>) request.getAttribute("inside"); if (isInside == null) { isInside = new HashMap<String, Boolean>(); request.setAttribute("inside", isInside); } isInside.put(section.getDisplayName(), Boolean.TRUE); isInside.put(section.getInternalName(), Boolean.TRUE); } /** * Removes the last parent section used to render the given * {@code request} so far. */ @SuppressWarnings("unchecked") protected static void removeLastParentSection(HttpServletRequest request) { List<Section> parents = (List<Section>) request.getAttribute(PARENT_SECTIONS_ATTRIBUTE); Section section = parents.remove(parents.size() - 1); ((Map<String, Boolean>) request.getAttribute("inside")).remove(section.getDisplayName()); ((Map<String, Boolean>) request.getAttribute("inside")).remove(section.getInternalName()); } /** * Returns a modifiable set of all the objects used to render the given * {@code request}. */ public static Set<Object> getRenderedObjects(HttpServletRequest request) { @SuppressWarnings("unchecked") Set<Object> rendered = (Set<Object>) request.getAttribute(RENDERED_OBJECTS_ATTRIBUTE); if (rendered == null) { rendered = new LinkedHashSet<Object>(); request.setAttribute(RENDERED_OBJECTS_ATTRIBUTE, rendered); } return rendered; } /** * Returns the map of object substitutions for the given * {@code request}. */ public static Map<UUID, Object> getSubstitutions(HttpServletRequest request) { @SuppressWarnings("unchecked") Map<UUID, Object> substitutions = (Map<UUID, Object>) request.getAttribute(SUBSTITUTIONS_ATTRIBUTE); if (substitutions == null) { substitutions = new HashMap<UUID, Object>(); request.setAttribute(SUBSTITUTIONS_ATTRIBUTE, substitutions); } return substitutions; } /** * Creates marker HTML that can be inserted into the page to identify * activity with the given {@code name} and {@code data}. * * @param name Nonnull. * @param data Nullable. * @return Nonnull. */ public static String createMarkerHtml(String name, Map<String, String> data) { Preconditions.checkNotNull(name); StringBuilder marker = new StringBuilder(); marker.append("<!--"); marker.append(name); if (data != null) { marker.append(" "); marker.append(ObjectUtils.toJson(data).replace("--", "\\u002d\\u002d")); } marker.append("-->"); return marker.toString(); } /** * Gets the entry view class associated with the given {@code request}. * First checks for the special parameter "_embed" and if set to true will * immediately return {@link EmbedEntryView}. Otherwise it falls back to * the request attribute {@link #ENTRY_VIEW_CLASS_ATTRIBUTE}. * * @param request Never {@code null}. * @return The entry view class for the current request. */ static Class<?> getEntryViewClass(HttpServletRequest request) { Class<?> viewClass = null; // special case to support module embeds on 3rd party pages // takes precedence over all if (ObjectUtils.to(boolean.class, request.getParameter("_embed"))) { viewClass = EmbedEntryView.class; } // and finally fall back to the request attribute. if (viewClass == null) { viewClass = (Class<?>) request.getAttribute(ENTRY_VIEW_CLASS_ATTRIBUTE); } return viewClass; } /** * Sets the page entry view class associated with the given {@code request}. * * @param request Never {@code null}. * @param viewClass The entry view class to set. */ public static void setEntryViewClass(HttpServletRequest request, Class<?> viewClass) { Preconditions.checkNotNull(request); request.setAttribute(ENTRY_VIEW_CLASS_ATTRIBUTE, viewClass); } /** * Returns the view template loader associated with the given * {@code request}, creating one if necessary. * * @param request Nonnull. * @return Nonnull. */ public static ViewTemplateLoader getViewTemplateLoader(HttpServletRequest request) { Preconditions.checkNotNull(request); ViewTemplateLoader loader = (ViewTemplateLoader) request.getAttribute(VIEW_TEMPLATE_LOADER_ATTRIBUTE); if (loader == null) { loader = ServletViewTemplateLoader.getInstance(request.getServletContext()); setViewTemplateLoader(request, loader); } return loader; } /** * Sets the view template loader to be used within the given * {@code request}. * * @param request Nonnull. * @param loader Nullable. */ public static void setViewTemplateLoader(HttpServletRequest request, ViewTemplateLoader loader) { Preconditions.checkNotNull(request); request.setAttribute(VIEW_TEMPLATE_LOADER_ATTRIBUTE, loader); } // --- AbstractFilter support --- @Override public Iterable<Class<? extends Filter>> dependencies() { List<Class<? extends Filter>> dependencies = new ArrayList<Class<? extends Filter>>(); dependencies.add(FrameFilter.class); dependencies.add(ApplicationFilter.class); dependencies.add(RemoteWidgetFilter.class); dependencies.add(AuthenticationFilter.class); dependencies.add(com.psddev.cms.tool.ScheduleFilter.class); dependencies.add(com.psddev.dari.util.FormFilter.class); dependencies.add(com.psddev.dari.util.FrameFilter.class); dependencies.add(com.psddev.dari.util.RoutingFilter.class); dependencies.add(FieldAccessFilter.class); dependencies.add(StorageItemFilter.class); return dependencies; } private static boolean redirectIfFixedPath(HttpServletRequest request, HttpServletResponse response) throws IOException { String path = (String) request.getAttribute(FIXED_PATH_ATTRIBUTE); if (path == null) { return false; } else { String queryString = request.getQueryString(); if (queryString != null) { path += "?" + queryString; } JspUtils.redirectPermanently(request, response, path); return true; } } @Override protected void doInit() throws Exception { poweredBy = Settings.getOrDefault(boolean.class, "brightspot/poweredBy", Boolean.TRUE); } @Override protected void doError( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { doForward(request, response, chain); } @Override protected void doForward( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { request.removeAttribute(MAIN_OBJECT_CHECKED_ATTRIBUTE); request.removeAttribute(PAGE_CHECKED_ATTRIBUTE); request.removeAttribute(PROFILE_CHECKED_ATTRIBUTE); request.removeAttribute(SITE_CHECKED_ATTRIBUTE); doRequest(request, response, chain); } @Override protected void doInclude( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws Exception { if (Static.isInlineEditingAllContents(request)) { response = new LazyWriterResponse(request, response); } try { super.doInclude(request, response, chain); } finally { if (response instanceof LazyWriterResponse) { ((LazyWriterResponse) response).getLazyWriter().writePending(); } } } @Override protected void doRequest( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { if (poweredBy) { response.setHeader("X-Powered-By", "Brightspot"); } if (request.getMethod().equalsIgnoreCase("HEAD") && ObjectUtils.to(boolean.class, request.getHeader("Brightspot-Main-Object-Id-Query"))) { Object mainObject = Static.getMainObject(request); if (mainObject != null) { response.setHeader("Brightspot-Main-Object-Id", State.getInstance(mainObject).getId().toString()); } return; } if (Static.isInlineEditingAllContents(request)) { try { JspBufferFilter.Static.overrideBuffer(0); doRequestForReal(request, response, chain); } finally { JspBufferFilter.Static.restoreBuffer(); } } else { doRequestForReal(request, response, chain); } } @SuppressWarnings("deprecation") private void doRequestForReal( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { Profile profile = Static.getProfile(request); Variation.Static.applyAll(TypeDefinition.getInstance(Record.class).newInstance(), profile); if (redirectIfFixedPath(request, response)) { return; } ToolUser user = AuthenticationFilter.Static.getInsecureToolUser(request); request.setAttribute("toolUser", user); VaryingDatabase varying = new VaryingDatabase(); varying.setDelegate(Database.Static.getDefault()); varying.setRequest(request); varying.setProfile(profile); Database.Static.overrideDefault(varying); Writer writer = null; try { String servletPath = request.getServletPath(); String externalUrl = Directory.extractExternalUrl(servletPath); if (externalUrl != null) { if (Directory.Static.findByPath( Static.getSite(request), servletPath.endsWith("/") ? servletPath + "index" : servletPath) != null) { response.sendRedirect(externalUrl); } else { response.setStatus(HttpServletResponse.SC_NOT_FOUND); } return; } // Serve a special robots.txt file for non-production. if (servletPath.equals("/robots.txt") && !Settings.isProduction()) { response.setContentType("text/plain"); writer = response.getWriter(); writer.write("User-agent: *\n"); writer.write("Disallow: /\n"); return; // Render a single section. } else if (servletPath.startsWith("/_render")) { UUID sectionId = ObjectUtils.to(UUID.class, request.getParameter("_sectionId")); Section section = Query.findById(Section.class, sectionId); if (section != null) { writeSection(request, response, response.getWriter(), section); } return; // Strip the special directory suffix. } else if (servletPath.endsWith("/index")) { JspUtils.redirectPermanently(request, response, servletPath.substring(0, servletPath.length() - 5)); return; } // Global prefix? String prefix = Settings.get(String.class, "cms/db/directoryPrefix"); if (!ObjectUtils.isBlank(prefix)) { Static.setPath(request, StringUtils.ensureEnd(prefix, "/") + servletPath); } Site site = Static.getSite(request); if (redirectIfFixedPath(request, response)) { return; } Object mainObject = Static.getMainObject(request); if (redirectIfFixedPath(request, response)) { return; } else { HttpServletRequest newRequest = (HttpServletRequest) request.getAttribute(NEW_REQUEST_ATTRIBUTE); if (newRequest != null) { request = newRequest; } } if (Static.isPreview(request) || user != null) { response.setHeader("Cache-Control", "private, no-cache, max-age=0, must-revalidate, no-store"); response.setHeader("Brightspot-Cache", "none"); } // Not handled by the CMS. if (mainObject == null) { chain.doFilter(request, response); return; } // If mainObject has a redirect path AND a permalink and the // current request is the redirect path, then redirect to the // permalink. Directory.Path redirectPath = null; Directory.PathType redirectType = null; for (Directory.Path p : State.getInstance(mainObject).as(Directory.Data.class).getPaths()) { if ((p.getType() == Directory.PathType.REDIRECT || p.getType() == Directory.PathType.REDIRECT_TEMPORARY) && ObjectUtils.equals(p.getSite(), site)) { String path = p.getPath(); String requestPath = Static.getPath(request); if (requestPath.equalsIgnoreCase(path)) { redirectType = p.getType(); } else { // handle wildcards int wildcardIndex = path.indexOf("/*"); if (wildcardIndex > -1) { String purePath = path.substring(0, wildcardIndex); if (requestPath.length() >= purePath.length()) { String requestPurePath = requestPath.substring(0, wildcardIndex); if (requestPurePath.equalsIgnoreCase(purePath)) { String requestPathLeftover = requestPath.substring(wildcardIndex, requestPath.length()); if (requestPathLeftover.isEmpty()) { redirectType = Directory.PathType.REDIRECT; } else { String wildcard = path.substring(wildcardIndex, path.length()); if (wildcard.equals("/**") || (wildcard.equals("/*") && requestPathLeftover.split("/").length < 3)) { redirectType = Directory.PathType.REDIRECT; } } } } } } } else if (p.getType() == Directory.PathType.PERMALINK && ObjectUtils.equals(p.getSite(), site)) { redirectPath = p; } } if (redirectType != null && redirectPath != null) { String rp = StringUtils.removeEnd(StringUtils.removeEnd(redirectPath.getPath(), "*"), "*"); if (redirectType == Directory.PathType.REDIRECT) { JspUtils.redirectPermanently(request, response, site != null ? site.getPrimaryUrl() + rp : rp); } else { JspUtils.redirect(request, response, site != null ? site.getPrimaryUrl() + rp : rp); } return; } Static.pushObject(request, mainObject); final State mainState = State.getInstance(mainObject); // Fake the request path in preview mode in case the servlets // depend on it. if (Static.isPreview(request)) { String previewPath = request.getParameter("_previewPath"); if (!ObjectUtils.isBlank(previewPath)) { int colonAt = previewPath.indexOf(':'); if (colonAt > -1) { Site previewSite = Query .from(Site.class) .where("_id = ?", ObjectUtils.to(UUID.class, previewPath.substring(0, colonAt))) .first(); if (previewSite != null) { Static.setSite(request, previewSite); } previewPath = previewPath.substring(colonAt + 1); } final String finalPreviewPath = previewPath; request = new HttpServletRequestWrapper(request) { @Override public String getRequestURI() { return getContextPath() + getServletPath(); } @Override public StringBuffer getRequestURL() { return new StringBuffer(getRequestURI()); } @Override public String getServletPath() { return finalPreviewPath; } }; } } if (!Static.isPreview(request) && !mainState.isVisible()) { SCHEDULED: { if (user != null) { Schedule currentSchedule = user.getCurrentSchedule(); if (currentSchedule != null && Query.from(Draft.class).where("schedule = ? and objectId = ?", currentSchedule, mainState.getId()).first() != null) { break SCHEDULED; } } CmsTool cms = Query.from(CmsTool.class).first(); if (user == null || (cms != null && cms.isDisableInvisibleContentPreview())) { if (Settings.isProduction()) { chain.doFilter(request, response); return; } else { throw new IllegalStateException(String.format( "[%s] isn't visible!", mainState.getId())); } } } } // If showing an invisible item, make sure all nested invisible // items show up too. if (!mainState.isVisible() || Static.isPreview(request)) { mainState.setResolveInvisible(true); } ObjectType mainType = mainState.getType(); Page page = Static.getPage(request); if (page == null && mainType != null && !ObjectUtils.isBlank(mainType.as(Renderer.TypeModification.class).getPath())) { page = Application.Static.getInstance(CmsTool.class).getModulePreviewTemplate(); } // SEO and <head>. Map<String, Object> seo = new HashMap<String, Object>(); Seo.ObjectModification seoData = mainState.as(Seo.ObjectModification.class); String seoTitle = seoData.findTitle(); String seoDescription = seoData.findDescription(); String seoRobots = seoData.findRobotsString(); Set<String> seoKeywords = seoData.findKeywords(); String seoKeywordsString = null; request.setAttribute("seo", seo); seo.put("title", seoTitle); seo.put("description", seoDescription); seo.put("robots", seoRobots); if (seoKeywords != null) { seoKeywordsString = seoKeywords.toString(); seo.put("keywords", seoKeywords); seo.put("keywordsString", seoKeywordsString); } PageStage stage = new PageStage(getServletContext(), request); request.setAttribute("stage", stage); stage.setMetaProperty("og:type", mainType.as(Seo.TypeModification.class).getOpenGraphType()); if (mainType != null && !ObjectUtils.isBlank(mainType.as(Renderer.TypeModification.class).getEmbedPath())) { stage.findOrCreateHeadElement("link", "rel", "alternate", "type", "application/json+oembed") .getAttributes() .put("href", JspUtils.getAbsoluteUrl(request, "", "_embed", true, "_format", "oembed")); } stage.setTitle(seoTitle); stage.setDescription(seoDescription); stage.setMetaName("robots", seoRobots); stage.setMetaName("keywords", seoKeywordsString); stage.update(mainObject); // Try to set the right content type based on the extension. String contentType = URLConnection.getFileNameMap().getContentTypeFor(servletPath); response.setContentType((ObjectUtils.isBlank(contentType) ? "text/html" : contentType) + ";charset=UTF-8"); // Disable Webkit's XSS Auditor since it often interferes with // how preview works. if (Static.isPreview(request)) { response.setHeader("X-XSS-Protection", "0"); } // Render the page. if (Static.isInlineEditingAllContents(request)) { response = new LazyWriterResponse(request, response); } writer = new HtmlWriter(response.getWriter()); ((HtmlWriter) writer).putAllStandardDefaults(); request.setAttribute("sections", new PullThroughCache<String, Section>() { @Override protected Section produce(String name) { return Query.from(Section.class).where("internalName = ?", name).first(); } }); beginPage(request, response, writer, page); if (!response.isCommitted()) { request.getSession(); } String context = request.getParameter("_context"); boolean contextNotBlank = !ObjectUtils.isBlank(context); boolean embed = ObjectUtils.coalesce( ObjectUtils.to(Boolean.class, request.getParameter("_embed")), contextNotBlank); String layoutPath = findLayoutPath(mainObject, embed); if (page != null && ObjectUtils.isBlank(layoutPath)) { layoutPath = findLayoutPath(page, embed); if (!embed && ObjectUtils.isBlank(layoutPath)) { layoutPath = page.getRendererPath(); } } if (ObjectUtils.isBlank(layoutPath) && Static.isPreview(request)) { layoutPath = findLayoutPath(mainObject, true); } String typePath = mainType.as(Renderer.TypeModification.class).getPath(); boolean rendered = false; try { ContextTag.Static.pushContext(request, contextNotBlank ? context : (embed ? EMBED_OBJECT_RENDERER_CONTEXT : MAIN_OBJECT_RENDERER_CONTEXT)); if (!ObjectUtils.isBlank(layoutPath)) { rendered = true; JspUtils.include(request, response, writer, StringUtils.ensureStart(layoutPath, "/")); } else if (page != null) { Page.Layout layout = page.getLayout(); if (layout != null) { rendered = true; renderSection(request, response, writer, layout.getOutermostSection()); } } if (!rendered && !embed && !ObjectUtils.isBlank(typePath)) { rendered = true; JspUtils.include(request, response, writer, StringUtils.ensureStart(typePath, "/")); } if (!rendered && mainObject instanceof Renderer) { rendered = true; ((Renderer) mainObject).renderObject(request, response, (HtmlWriter) writer); } if (!rendered && tryProcessView(request, response, writer, mainObject)) { rendered = true; } } finally { ContextTag.Static.popContext(request); } if (!rendered) { if (Settings.isProduction()) { chain.doFilter(request, response); return; } else { StringBuilder message = new StringBuilder(); if (embed) { message.append("@Renderer.EmbedPath required on ["); message.append(mainObject.getClass().getName()); message.append("] to render it in ["); message.append(ObjectUtils.isBlank(context) ? "_embed" : context); message.append("] context"); } else { message.append("@Renderer.Path or @Renderer.LayoutPath required on ["); message.append(mainObject.getClass().getName()); message.append("] to render it"); } message.append(" (Object: ["); message.append(mainState.getLabel()); message.append("], ID: ["); message.append(mainState.getId().toString().replaceAll("-", "")); message.append("])!"); throw new IllegalStateException(message.toString()); } } endPage(request, response, writer, page); } finally { Database.Static.restoreDefault(); if (response instanceof LazyWriterResponse) { ((LazyWriterResponse) response).getLazyWriter().writePending(); } } if (Settings.isDebug() || (Static.isPreview(request) && !Boolean.TRUE.equals(request.getAttribute(PERSISTENT_PREVIEW_ATTRIBUTE)))) { return; } String contentType = response.getContentType(); if (contentType == null || !StringUtils.ensureEnd(contentType, ";").startsWith("text/html;")) { return; } Object mainObject = PageFilter.Static.getMainObject(request); if (mainObject != null && user != null) { if (!JspUtils.isError(request)) { user.saveAction(request, mainObject); } if (user.getInlineEditing() != ToolUser.InlineEditing.DISABLED) { ToolPageContext page = new ToolPageContext(getServletContext(), request, response); State mainState = State.getInstance(mainObject); page.setDelegate(writer instanceof HtmlWriter ? (HtmlWriter) writer : new HtmlWriter(writer)); Map<String, String> markerMap = new CompactMap<>(); markerMap.put("id", mainState.getId().toString()); markerMap.put("label", mainState.getLabel()); markerMap.put("typeLabel", mainState.getType().getLabel()); page.write(createMarkerHtml("BrightspotCmsMainObject", markerMap)); page.writeStart("iframe", "class", "BrightspotCmsInlineEditor", "id", "bsp-inlineEditorContents", "onload", "this.style.visibility = 'visible';", "scrolling", "no", "src", page.cmsUrl("/inlineEditor", "id", mainState.getId()), "style", page.cssString( "border", "none", "height", 0, "left", 0, "margin", 0, "pointer-events", "none", "position", "absolute", "top", 0, "visibility", "hidden", "width", "100%", "z-index", 1000000)); page.writeEnd(); if (response instanceof LazyWriterResponse) { ((LazyWriterResponse) response).getLazyWriter().writePending(); } } } } private String findLayoutPath(Object object, boolean embed) { ObjectType type = State.getInstance(object).getType(); if (type != null) { Renderer.TypeModification rendererData = type.as(Renderer.TypeModification.class); return embed ? rendererData.getEmbedPath() : rendererData.getLayoutPath(); } else { return null; } } /** Renders the beginning of the given {@code page}. */ protected static void beginPage( HttpServletRequest request, HttpServletResponse response, Writer writer, Page page) throws IOException, ServletException { } /** Renders the end of the given {@code page}. */ protected static void endPage( HttpServletRequest request, HttpServletResponse response, Writer writer, Page page) throws IOException, ServletException { } /** Renders the given {@code section}. */ public static void renderSection( HttpServletRequest request, HttpServletResponse response, Writer writer, Section section) throws IOException, ServletException { if (isAborted(request)) { return; } try { Profiler.Static.startThreadEvent("Render", section); if (section != null) { Object substitution = getSubstitutions(request).get(section.getId()); if (substitution != null) { if (substitution instanceof Section) { section = (Section) substitution; } else { ContentSection substitutionSection = new ContentSection(); substitutionSection.setContent(substitution); section = substitutionSection; } } } long cacheDuration = section != null ? section.getCacheDuration() : 0; if (cacheDuration > 0) { SectionCacheKey key = new SectionCacheKey(); key.sectionId = section.getId(); key.cacheDuration = cacheDuration; key.request = request; key.response = response; key.section = section; writer.write(SECTION_CACHE.get(key)); } else { Section previousSection = getCurrentSection(request); try { setCurrentSection(request, section); writeSection(request, response, writer, section); } finally { setCurrentSection(request, previousSection); } } } finally { Profiler.Static.stopThreadEvent(); } } /** * Processes and writes the given {@code section} to the given * {@code writer}. */ @SuppressWarnings("all") private static void writeSection( HttpServletRequest request, HttpServletResponse response, Writer writer, Section section) throws IOException, ServletException { // Container section - begin, child sections, then end. if (section instanceof ContainerSection) { ContainerSection container = (ContainerSection) section; List<Section> children = container.getChildren(); try { addParentSection(request, container); beginContainer(request, response, writer, container); for (Section child : children) { renderSection(request, response, writer, child); if (isAborted(request)) { return; } } endContainer(request, response, writer, container); } finally { removeLastParentSection(request); } // Script section may be associated with an object. } else if (section instanceof ScriptSection) { Object object; if (section instanceof MainSection) { object = getMainObject(request); } else if (section instanceof ContentSection) { object = ((ContentSection) section).getContent(); } else { object = null; } renderObjectWithSection(request, response, writer, object, (ScriptSection) section); } } /** * Key for {@link #SECTION_CACHE} that contains extra information * like the request object. */ private static class SectionCacheKey { public UUID sectionId; public long cacheDuration; public HttpServletRequest request; public HttpServletResponse response; public Section section; @Override public boolean equals(Object other) { return this == other || (other instanceof SectionCacheKey && sectionId.equals(((SectionCacheKey) other).sectionId)); } @Override public int hashCode() { return sectionId.hashCode(); } } /** Cache that contains output of each sections. */ private static final PullThroughCache<SectionCacheKey, String> SECTION_CACHE = new PullThroughCache<SectionCacheKey, String>() { @Override protected boolean isExpired(SectionCacheKey key, Date lastProduced) { return System.currentTimeMillis() - lastProduced.getTime() > key.cacheDuration; } @Override protected String produce(SectionCacheKey key) throws IOException, ServletException { try { StringWriter writer = new StringWriter(); writeSection(key.request, key.response, writer, key.section); return writer.toString(); } finally { key.request = null; key.response = null; key.section = null; } } }; /* * 1. Find ViewModel class (check the different view types, etc.) * 2. Create custom ViewModelCreator * 3. Create a ViewResponse * 4. Create ViewModel from ViewModelCreator and pass ViewResponse * 5. Find the ViewRenderer for the ViewModel * 6. Render the ViewModel * 7. Update the real HTTP response headers based on the ViewResponse * 8. Write the output to the real HTTP response */ private static <T> boolean tryProcessView( HttpServletRequest request, HttpServletResponse response, Writer writer, T object) throws IOException, ServletException { if (object == null) { return false; } String selectedViewType = null; // 1. Find ViewModel class (check the different view types, etc.) Class<? extends ViewModel<? super T>> viewModelClass = null; String viewType = Static.getViewType(request); List<String> viewTypes = new ArrayList<>(); if (!ObjectUtils.isBlank(viewType)) { viewModelClass = ViewModel.findViewModelClass(viewType, object); viewTypes.add(viewType); if (viewModelClass == null) { if (EMBED_VIEW_TYPE.equals(viewType)) { viewModelClass = ViewModel.findViewModelClass(EmbedEntryView.class, object); viewTypes.add(EmbedEntryView.class.getName()); } } else { selectedViewType = viewType; } } else { Class<?> entryViewClass = getEntryViewClass(request); if (entryViewClass != null) { viewModelClass = ViewModel.findViewModelClass(entryViewClass, object); viewTypes.add(entryViewClass.getName()); } if (viewModelClass == null) { // Try to create a view for the PREVIEW_VIEW_TYPE... if (Static.isPreview(request)) { viewModelClass = ViewModel.findViewModelClass(PreviewEntryView.class, object); viewTypes.add(PreviewEntryView.class.getName()); if (viewModelClass == null) { viewModelClass = ViewModel.findViewModelClass(PREVIEW_VIEW_TYPE, object); viewTypes.add(PREVIEW_VIEW_TYPE); } if (viewModelClass != null) { selectedViewType = PREVIEW_VIEW_TYPE; } } // ...but still always fallback to PAGE_VIEW_TYPE if no preview found. if (viewModelClass == null) { viewModelClass = ViewModel.findViewModelClass(PageEntryView.class, object); viewTypes.add(PageEntryView.class.getName()); if (viewModelClass == null) { viewModelClass = ViewModel.findViewModelClass(PAGE_VIEW_TYPE, object); viewTypes.add(PAGE_VIEW_TYPE); } if (viewModelClass != null) { selectedViewType = PAGE_VIEW_TYPE; } } } } if (viewModelClass == null) { if (object.getClass().isAnnotationPresent(ViewBinding.class)) { LOGGER.warn("Could not find view model for object of type [" + object.getClass().getName() + "] and view of type [" + StringUtils.join(viewTypes, ", or ") + "]!"); } } if (viewModelClass == null) { return tryProcessViewLegacy(request, response, writer, object); } // 2. Create custom ViewModelCreator ViewModelCreator viewModelCreator = new ServletViewModelCreator(request); // 3. Create a ViewResponse ViewResponse viewResponse = new ViewResponse(); // 4. Create ViewModel from ViewModelCreator and pass ViewResponse ViewModel<? super T> viewModel = null; try { viewModel = viewModelCreator.createViewModel(viewModelClass, object, viewResponse); if (viewModel == null) { LOGGER.warn("Failed to create view model of type [" + viewModelClass.getName() + "] for object of type [" + object.getClass().getName() + "] and view of type [" + selectedViewType + "]!"); } } catch (RuntimeException e) { ViewResponse vr = ViewResponse.findInExceptionChain(e); if (vr != null) { viewResponse = vr; } else { throw e; } } String output = null; if (viewModel != null) { // 5. Find the ViewRenderer for the ViewModel ViewRenderer renderer; if ("json".equals(request.getParameter("_renderer"))) { JsonViewRenderer jsonViewRenderer = new JsonViewRenderer(); jsonViewRenderer.setIndented(!Settings.isProduction()); jsonViewRenderer.setIncludeClassNames(!Settings.isProduction()); jsonViewRenderer.setDisallowMixedOutput(true); renderer = jsonViewRenderer; } else { renderer = ViewRenderer.createRenderer(viewModel); } // 6. Render the ViewModel if (renderer != null) { String contentType = renderer.getContentType(); if (contentType != null) { response.setContentType(contentType); } try { ViewOutput result = renderer.render(viewModel, getViewTemplateLoader(request)); output = result.get(); } catch (RuntimeException e) { ViewResponse vr = ViewResponse.findInExceptionChain(e); if (vr != null) { // These will usually be the same, but an implementer could potentially throw a different one viewResponse = vr; } else { throw e; } } } else { LOGGER.warn("Could not create view renderer for view of type [" + viewModel.getClass().getName() + "]!"); } } // 7. Update the real HTTP response headers based on the ViewResponse updateViewResponse(request, (HttpServletResponse) JspUtils.getHeaderResponse(request, response), viewResponse); // 8. Write the output to the real HTTP response if (output != null) { writer.write(output); } return true; } /* * 1. Find ViewCreator class (check the different view types, etc.) * 2. Create ViewCreator * 3. Create a ViewRequest based on the ViewCreator class * 4. Create a ViewResponse * 5. Call ViewCreator#processRequest * 6. Update the real response based on the view response. * 7. Check if the request should continue to be processed. * 8. Create the View * 9. Render the View */ @Deprecated private static <T> boolean tryProcessViewLegacy( HttpServletRequest request, HttpServletResponse response, Writer writer, T object) throws IOException, ServletException { String selectedViewType = null; // 1. Find ViewCreator class (check the different view types, etc.) Class<? extends ViewCreator<? super T, ?, ? super Object>> viewCreatorClass = null; String viewType = request.getParameter(VIEW_TYPE_PARAMETER); if (!ObjectUtils.isBlank(viewType)) { viewCreatorClass = ViewCreator.findCreatorClass(object, null, viewType, null); if (viewCreatorClass == null) { LOGGER.warn("Could not find view creator for object of type [" + object.getClass().getName() + "] and view of type [" + viewType + "]!"); } else { selectedViewType = viewType; } } else { List<String> viewTypes = new ArrayList<>(); // Try to create a view for the PREVIEW_VIEW_TYPE... if (Static.isPreview(request)) { viewCreatorClass = ViewCreator.findCreatorClass(object, null, PREVIEW_VIEW_TYPE, null); viewTypes.add(PREVIEW_VIEW_TYPE); if (viewCreatorClass != null) { selectedViewType = PREVIEW_VIEW_TYPE; } } // ...but still always fallback to PAGE_VIEW_TYPE if no preview found. if (viewCreatorClass == null) { viewCreatorClass = ViewCreator.findCreatorClass(object, null, PAGE_VIEW_TYPE, null); viewTypes.add(PAGE_VIEW_TYPE); if (viewCreatorClass != null) { selectedViewType = PAGE_VIEW_TYPE; } } if (viewCreatorClass == null) { PageViewClass annotation = object.getClass().getAnnotation(PageViewClass.class); Class<?> layoutViewClass = annotation != null ? annotation.value() : null; if (layoutViewClass != null) { viewCreatorClass = ViewCreator.findCreatorClass(object, layoutViewClass, null, null); viewTypes.add(layoutViewClass.getName()); if (viewCreatorClass != null) { selectedViewType = layoutViewClass.getName(); } } } if (viewCreatorClass == null) { if (object.getClass().isAnnotationPresent(ViewMapping.class)) { LOGGER.warn("Could not find view creator for object of type [" + object.getClass().getName() + "] and view of type [" + StringUtils.join(viewTypes, ", or ") + "]!"); } } } if (viewCreatorClass == null) { return false; } // 2. Create ViewCreator ViewCreator<? super T, ?, ? super Object> viewCreator = TypeDefinition.getInstance(viewCreatorClass).newInstance(); if (viewCreator == null) { LOGGER.warn("Failed to create view creator of type [" + viewCreatorClass.getName() + "] for object of type [" + object.getClass().getName() + "] and view of type [" + selectedViewType + "]!"); return false; } // 3. Create a ViewRequest based on the ViewCreator class Object viewRequest = createViewRequest(request, viewCreator); if (viewRequest == null) { LOGGER.warn("Failed to create view request for object of type [" + object.getClass().getName() + "] and view creator of type [" + viewCreator.getClass().getName() + "]!"); return false; } // 4. Create a ViewResponse ViewResponse viewResponse = new ViewResponse(); // 5. Call ViewCreator#processRequest boolean continueProcessing = viewCreator.processRequest(viewRequest, viewResponse); // 6. Update the real response based on the view response. updateViewResponse(request, (HttpServletResponse) JspUtils.getHeaderResponse(request, response), viewResponse); // 7. Check if the request should continue to be processed. if (!continueProcessing) { return true; } // 8. Create the View Object view = viewCreator.createView(object, viewRequest); if (view == null) { LOGGER.warn("Failed to create view from model of type [" + object.getClass().toString() + "] and view creator of type [" + viewCreator.getClass().getName() + "]!"); return true; } // 9. Render the View ViewRenderer renderer; if ("json".equals(request.getParameter("_renderer"))) { JsonViewRenderer jsonViewRenderer = new JsonViewRenderer(); jsonViewRenderer.setIndented(!Settings.isProduction()); jsonViewRenderer.setIncludeClassNames(!Settings.isProduction()); jsonViewRenderer.setDisallowMixedOutput(true); renderer = jsonViewRenderer; response.setContentType("application/json"); } else { renderer = ViewRenderer.createRenderer(view); } if (renderer != null) { ViewOutput result = renderer.render(view, getViewTemplateLoader(request)); String output = result.get(); if (output != null) { writer.write(output); } } else { LOGGER.warn("Could not create view renderer for view of type [" + view.getClass().getName() + "]!"); } return true; } private static Object createViewRequest(HttpServletRequest request, ViewCreator<?, ?, ? super Object> viewCreator) { if (viewCreator != null) { Object viewRequest; Class<?> viewRequestClass = TypeDefinition.getInstance(viewCreator.getClass()) .getInferredGenericTypeArgumentClass(ViewCreator.class, 2); if (ViewRequest.class.equals(viewRequestClass) && AbstractViewCreator.class.isAssignableFrom(viewCreator.getClass())) { // Legacy ViewRequest support viewRequest = new ServletViewRequest(request); } else { viewRequest = createViewRequest(request, viewRequestClass); } return viewRequest; } return null; } private static Object createViewRequest(HttpServletRequest request, Class<?> viewRequestClass) { Converter converter = new Converter(); converter.putAllStandardFunctions(); try { TypeDefinition<?> viewRequestDefinition = TypeDefinition.getInstance(viewRequestClass); Object viewRequest = viewRequestDefinition.newInstance(); for (Map.Entry<String, List<Field>> entry : viewRequestDefinition.getAllSerializableFields().entrySet()) { Field field = entry.getValue().get(entry.getValue().size() - 1); String fieldName = field.getName(); Object fieldValue = null; // check for annotation processors. for (Annotation viewRequestAnnotation : field.getAnnotations()) { Class<?> annotationClass = viewRequestAnnotation.annotationType(); ServletViewRequestAnnotationProcessorClass annotation = annotationClass.getAnnotation( ServletViewRequestAnnotationProcessorClass.class); if (annotation != null) { Class<? extends ServletViewRequestAnnotationProcessor<? extends Annotation>> annotationProcessorClass = annotation.value(); if (annotationProcessorClass != null) { @SuppressWarnings("unchecked") ServletViewRequestAnnotationProcessor<Annotation> annotationProcessor = (ServletViewRequestAnnotationProcessor<Annotation>) TypeDefinition.getInstance(annotationProcessorClass).newInstance(); fieldValue = annotationProcessor.getValue(request, fieldName, viewRequestAnnotation); break; } } } if (fieldValue != null) { try { // Handle the case where the field value is a collection but the field type is not. if (fieldValue instanceof Collection && !Collection.class.isAssignableFrom(field.getType())) { if (!((Collection<?>) fieldValue).isEmpty()) { // get the first value from the collection fieldValue = ((Collection<?>) fieldValue).iterator().next(); } else { fieldValue = null; } } field.set(viewRequest, converter.convert(field.getGenericType(), fieldValue)); } catch (IllegalAccessException ex) { throw new IllegalStateException(ex); } } } return viewRequest; } catch (RuntimeException e) { LOGGER.warn("Failed to create view request of type [" + viewRequestClass + "]. Cause: " + e.getMessage(), e); return null; } } // Copies the information on the ViewResponse to the actual http servlet response. private static void updateViewResponse(HttpServletRequest request, HttpServletResponse response, ViewResponse viewResponse) { Integer status = viewResponse.getStatus(); if (status != null) { response.setStatus(status); } for (Map.Entry<String, List<String>> entry : viewResponse.getHeaders().entrySet()) { String name = entry.getKey(); List<String> values = entry.getValue(); for (String value : values) { response.addHeader(name, value); } } viewResponse.getCookies().forEach(response::addCookie); viewResponse.getSignedCookies().forEach(cookie -> JspUtils.setSignedCookie(response, cookie)); String redirectUrl = viewResponse.getRedirectUri(); if (redirectUrl != null) { try { JspUtils.redirect(request, response, redirectUrl); } catch (IOException e) { // ignore } } } /** Renders the given {@code object}. */ public static void renderObject( HttpServletRequest request, HttpServletResponse response, Writer writer, Object object) throws IOException, ServletException { if (!tryProcessView(request, response, writer, object)) { if (object instanceof Section) { renderSection(request, response, writer, (Section) object); } else { renderObjectWithSection(request, response, writer, object, null); } } } /** Renders the given {@code object} using the given {@code section}. */ @SuppressWarnings("all") private static void renderObjectWithSection( HttpServletRequest request, HttpServletResponse response, Writer writer, Object object, ScriptSection section) throws IOException, ServletException { String engine; String script; if (section != null) { engine = section.getEngine(); script = section.getRendererPath(); } else { engine = null; script = null; } if (object != null) { Object substitution = getSubstitutions(request).get(State.getInstance(object).getId()); if (substitution != null) { object = substitution; } getRenderedObjects(request).add(object); // Engine not specified on section so fall back to the one // specified in the object type definition. if (ObjectUtils.isBlank(script)) { ObjectType type = State.getInstance(object).getType(); if (type != null) { Renderer.TypeModification typeRenderer = type.as(Renderer.TypeModification.class); engine = typeRenderer.getEngine(); script = typeRenderer.findContextualPath(request); } } } LazyWriter lazyWriter; String contentType = response.getContentType(); if (Static.isInlineEditingAllContents(request) && contentType != null && StringUtils.ensureEnd(contentType, ";").startsWith("text/html;")) { lazyWriter = new LazyWriter(request, writer); writer = lazyWriter; } else { lazyWriter = null; } try { if (object != null) { Static.pushObject(request, object); } if (lazyWriter != null) { Map<String, String> map = new HashMap<String, String>(); Object concrete = Static.peekConcreteObject(request); StringBuilder marker = new StringBuilder(); if (section != null) { map.put("sectionName", section.getName()); map.put("sectionId", section.getId().toString()); } if (concrete != null) { State state = State.getInstance(concrete); ObjectType stateType = state.getType(); map.put("id", state.getId().toString()); if (stateType != null) { map.put("typeLabel", stateType.getLabel()); } try { map.put("label", state.getLabel()); } catch (RuntimeException error) { // Not a big deal if label can't be retrieved. } } marker.append(createMarkerHtml("BrightspotCmsObjectBegin", map)); lazyWriter.writeLazily(marker.toString()); } if (ObjectUtils.isBlank(script) && object instanceof Renderer) { ((Renderer) object).renderObject( request, response, writer instanceof HtmlWriter ? (HtmlWriter) writer : new HtmlWriter(writer)); } else { renderScript(request, response, writer, engine, script); } } finally { if (object != null) { Static.popObject(request); } if (lazyWriter != null) { lazyWriter.writeLazily(createMarkerHtml("BrightspotCmsObjectEnd", null)); lazyWriter.writePending(); } } } // Renders the given script using the given engine. private static void renderScript( HttpServletRequest request, HttpServletResponse response, Writer writer, String engine, String script) throws IOException, ServletException { try { if ("RawText".equals(engine)) { writer.write(script); return; } else if (!ObjectUtils.isBlank(script)) { JspUtils.include(request, response, writer, StringUtils.ensureStart(script, "/")); return; } // Always catch the error so the page never looks broken // in production. } catch (Throwable ex) { if (Settings.isProduction()) { LOGGER.warn(String.format("Can't render [%s]!", script), ex); } else if (ex instanceof IOException) { throw (IOException) ex; } else if (ex instanceof ServletException) { throw (ServletException) ex; } else if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } else if (ex instanceof Error) { throw (Error) ex; } else { throw new RuntimeException(ex); } } } /** {@link PageFilter} utility methods. */ public static final class Static { private Static() { } /** Returns the path used to find the main object. */ public static String getPath(HttpServletRequest request) { getSite(request); String path = (String) request.getAttribute(PATH_ATTRIBUTE); return path != null ? path : request.getServletPath(); } /** Sets the path used to find the main object. */ public static void setPath(HttpServletRequest request, String path) { request.setAttribute(PATH_ATTRIBUTE, path); } private static void fixPath(HttpServletRequest request, String path) { request.setAttribute(FIXED_PATH_ATTRIBUTE, path); } /** Returns the site associated with the given {@code request}. */ public static Site getSite(HttpServletRequest request) { if (Boolean.TRUE.equals(request.getAttribute(SITE_CHECKED_ATTRIBUTE))) { return (Site) request.getAttribute(SITE_ATTRIBUTE); } request.setAttribute(SITE_CHECKED_ATTRIBUTE, Boolean.TRUE); Site site = null; String servletPath = request.getServletPath(); String absoluteUrl = JspUtils.getAbsoluteUrl(request, servletPath); Map.Entry<String, Site> entry = Site.Static.findByUrl(absoluteUrl); if (entry != null) { String path = absoluteUrl.substring(entry.getKey().length() - 1); if (Query.from(CmsTool.class).first().isRemoveTrailingSlashes()) { if ("/".equals(path)) { fixPath(request, servletPath.substring(0, servletPath.length() - 1)); } } else if (path.length() == 0) { fixPath(request, servletPath + "/"); } site = Query.from(Site.class).where("_id = ?", entry.getValue()).first(); setSite(request, site); setPath(request, path); } return site; } /** Sets the site associated with the given {@code request}. */ public static void setSite(HttpServletRequest request, Site site) { request.setAttribute(SITE_CHECKED_ATTRIBUTE, Boolean.TRUE); request.setAttribute(SITE_ATTRIBUTE, site); request.setAttribute("site", site); } /** * Returns {@code true} if the given {@code request} is for a preview. * * @param request Can't be {@code null}. */ public static boolean isPreview(HttpServletRequest request) { return Boolean.TRUE.equals(request.getAttribute(PREVIEW_ATTRIBUTE)); } /** Returns the main object associated with the given {@code request}. */ public static Object getMainObject(HttpServletRequest request) { if (Boolean.TRUE.equals(request.getAttribute(MAIN_OBJECT_CHECKED_ATTRIBUTE))) { return request.getAttribute(MAIN_OBJECT_ATTRIBUTE); } VaryingDatabase varying = new VaryingDatabase(); varying.setDelegate(Database.Static.getDefault()); varying.setRequest(request); varying.setProfile(getProfile(request)); Database.Static.overrideDefault(varying); try { request.setAttribute(MAIN_OBJECT_CHECKED_ATTRIBUTE, Boolean.TRUE); Object mainObject = null; String servletPath = request.getServletPath(); String path = getPath(request); Site site = getSite(request); // On preview request, manually create the main object based on // the post data. if (request.getServletPath().startsWith("/_preview")) { request.setAttribute(PREVIEW_ATTRIBUTE, Boolean.TRUE); UUID previewId = ObjectUtils.to(UUID.class, request.getParameter(PREVIEW_ID_PARAMETER)); if (previewId == null) { String previewIdString = path.substring(10); int slashAt = previewIdString.indexOf('/'); if (slashAt > -1) { previewIdString = previewIdString.substring(0, slashAt); } previewId = ObjectUtils.to(UUID.class, previewIdString); } if (previewId != null) { Map<UUID, Object> substitutions = getSubstitutions(request); if (ObjectUtils.to(Date.class, request.getParameter("_date")) == null) { String[] objectStrings = request.getParameterValues(PREVIEW_OBJECT_PARAMETER); if (objectStrings != null) { for (String objectString : objectStrings) { if (!ObjectUtils.isBlank(objectString)) { @SuppressWarnings("unchecked") Map<String, Object> objectMap = (Map<String, Object>) ObjectUtils.fromJson(objectString.trim()); ObjectType type = ObjectType.getInstance(ObjectUtils.to(UUID.class, objectMap.remove("_typeId"))); if (type != null) { Object object = type.createObject(ObjectUtils.to(UUID.class, objectMap.remove("_id"))); State objectState = State.getInstance(object); objectState.setResolveInvisible(true); objectState.setValues(objectMap); substitutions.put(objectState.getId(), object); } } } } } UUID mainObjectId = ObjectUtils.to(UUID.class, request.getParameter("_mainObjectId")); Object preview = Query .fromAll() .where("_id = ?", mainObjectId) .first(); if (preview == null) { preview = Query .fromAll() .where("_id = ?", previewId) .first(); } if (preview instanceof Draft) { mainObject = ((Draft) preview).recreate(); } else if (preview instanceof History) { mainObject = ((History) preview).getObject(); } else if (preview instanceof Preview) { Preview previewPreview = (Preview) preview; mainObject = previewPreview.getObject(); Site previewSite = previewPreview.getSite(); if (previewSite != null) { site = previewSite; setSite(request, site); } AuthenticationFilter.Static.setCurrentPreview(request, PageContextFilter.Static.getResponse(), previewPreview); } else if (mainObjectId != null) { mainObject = preview; } else { mainObject = substitutions.get(previewId); if (mainObject == null) { mainObject = preview; } } } if (mainObject != null) { Directory.Data dirData = State.getInstance(mainObject).as(Directory.Data.class); if (dirData.getRawPaths().isEmpty()) { dirData.addPath(null, "/_preview-" + previewId, Directory.PathType.PERMALINK); } Site previewSite = Query .from(Site.class) .where("_id = ?", request.getParameter(PREVIEW_SITE_ID_PARAMETER)) .first(); if (previewSite != null) { setSite(request, previewSite); } else { for (Directory.Path p : dirData.getPaths()) { if (Directory.PathType.PERMALINK.equals(p.getType())) { setSite(request, p.getSite()); break; } } } } } else { mainObject = Directory.Static.findByPath(site, path); if (mainObject == null) { mainObject = Directory.Static.findByPath(site, path + "/index"); // Index pages should have a trailing slash. if (mainObject != null) { // Except when told not to. if (Query.from(CmsTool.class).first().isRemoveTrailingSlashes()) { if (path.length() > 1 && path.endsWith("/")) { fixPath(request, servletPath.substring(0, servletPath.length() - 1)); } } else if (!path.endsWith("/")) { fixPath(request, servletPath + "/"); } } // Normal pages shouldn't have a trailing slash. } else if (path.endsWith("/")) { fixPath(request, servletPath.substring(0, servletPath.length() - 1)); } } // Case-insensitive path look-up. for (int i = 0, length = path.length(); i < length; ++ i) { if (Character.isUpperCase(path.charAt(i))) { String pathLc = path.toLowerCase(Locale.ENGLISH); if (Directory.Static.findObject(site, pathLc) != null) { fixPath(request, pathLc); } break; } } // Special fallback names. For example, given /path/to/file, // the following are checked: // // - /path/to/file/* // - /path/to/file/** // - /path/to/* // - /path/to/** // - /path/** // - /** String checkPath; int endMarker; if (path.endsWith("/")) { checkPath = path; endMarker = 0; } else { checkPath = path + "/"; endMarker = 1; } for (int i = 0; mainObject == null; ++ i) { int slashAt = checkPath.lastIndexOf("/"); if (slashAt < 0) { break; } else { checkPath = checkPath.substring(0, slashAt); } if (i <= endMarker) { mainObject = Directory.Static.findObject(site, checkPath + "/*"); } if (mainObject == null) { mainObject = Directory.Static.findObject(site, checkPath + "/**"); } if (mainObject instanceof Directory) { mainObject = null; } if (mainObject != null) { final String pathInfo = path.substring(checkPath.length()); if (Query.from(CmsTool.class).first().isRemoveTrailingSlashes()) { if ("/".equals(pathInfo)) { fixPath(request, servletPath.substring(0, servletPath.length() - 1)); } } else if (pathInfo.length() < 1) { fixPath(request, servletPath + "/"); } request.setAttribute(NEW_REQUEST_ATTRIBUTE, new HttpServletRequestWrapper(request) { @Override public String getPathInfo() { return pathInfo; } }); } } if (!Static.isPreview(request) && mainObject != null) { Preview preview = AuthenticationFilter.Static.getCurrentPreview(request); if (preview != null) { State mainState = State.getInstance(mainObject); if (mainState.getId().equals(preview.getObjectId())) { request.setAttribute(PREVIEW_ATTRIBUTE, Boolean.TRUE); request.setAttribute(PERSISTENT_PREVIEW_ATTRIBUTE, Boolean.TRUE); mainState.putAll(preview.getObjectValues()); } } } setMainObject(request, mainObject); return mainObject; } finally { Database.Static.restoreDefault(); } } /** Sets the main object associated with the given {@code request}. */ public static void setMainObject(HttpServletRequest request, Object mainObject) { request.setAttribute(MAIN_OBJECT_CHECKED_ATTRIBUTE, Boolean.TRUE); request.setAttribute(MAIN_OBJECT_ATTRIBUTE, mainObject); request.setAttribute("mainObject", mainObject); request.setAttribute("mainRecord", mainObject); request.setAttribute("mainContent", mainObject); } /** Returns the page used to render the given {@code request}. */ public static Page getPage(HttpServletRequest request) { if (Boolean.TRUE.equals(request.getAttribute(PAGE_CHECKED_ATTRIBUTE))) { return (Page) request.getAttribute(PAGE_ATTRIBUTE); } request.setAttribute(PAGE_CHECKED_ATTRIBUTE, Boolean.TRUE); Page page = null; Object mainObject = getMainObject(request); if (mainObject instanceof Page) { page = (Page) mainObject; } else { page = Template.Static.findRenderable(mainObject, getSite(request)); } setPage(request, page); return page; } /** Sets the page used to render the given {@code request}. */ public static void setPage(HttpServletRequest request, Page page) { request.setAttribute(PAGE_CHECKED_ATTRIBUTE, Boolean.TRUE); request.setAttribute(PAGE_ATTRIBUTE, page); request.setAttribute("template", page); } /** Returns the profile used to process the given {@code request}. */ public static Profile getProfile(HttpServletRequest request) { if (Boolean.TRUE.equals(request.getAttribute(PROFILE_CHECKED_ATTRIBUTE))) { return (Profile) request.getAttribute(PROFILE_ATTRIBUTE); } request.setAttribute(PROFILE_CHECKED_ATTRIBUTE, Boolean.TRUE); Profile profile = new Profile(); profile.setUserAgent(request.getHeader("User-Agent")); setProfile(request, profile); return profile; } /** Sets the profile used to process the given {@code request}. */ public static void setProfile(HttpServletRequest request, Profile profile) { request.setAttribute(PROFILE_CHECKED_ATTRIBUTE, Boolean.TRUE); request.setAttribute(PROFILE_ATTRIBUTE, profile); request.setAttribute("profile", profile); } /** * Pushes the given {@code object} to the list of objects that * are currently being rendered. */ public static void pushObject(HttpServletRequest request, Object object) { ErrorUtils.errorIfNull(object, "object"); @SuppressWarnings("unchecked") List<Object> objects = (List<Object>) request.getAttribute(OBJECTS_ATTRIBUTE); if (objects == null) { objects = new ArrayList<Object>(); request.setAttribute(OBJECTS_ATTRIBUTE, objects); } objects.add(object); request.setAttribute("content", object); request.setAttribute("record", object); request.setAttribute("object", object); request.setAttribute(CURRENT_OBJECT_ATTRIBUTE, object); } /** * Pops the last object from the list of objects that are currently * being rendered. */ public static Object popObject(HttpServletRequest request) { @SuppressWarnings("unchecked") List<Object> objects = (List<Object>) request.getAttribute(OBJECTS_ATTRIBUTE); if (objects == null || objects.isEmpty()) { return null; } else { Object popped = objects.remove(objects.size() - 1); Object object = peekObject(request); request.setAttribute("content", object); request.setAttribute("record", object); request.setAttribute("object", object); return popped; } } /** * Returns the last object from the list of objects that are currently * being rendered. */ public static Object peekObject(HttpServletRequest request) { @SuppressWarnings("unchecked") List<Object> objects = (List<Object>) request.getAttribute(OBJECTS_ATTRIBUTE); if (objects == null || objects.isEmpty()) { return null; } else { return objects.get(objects.size() - 1); } } /** * Returns the last concrete object from the list of objects that are * currently being rendered. */ public static Object peekConcreteObject(HttpServletRequest request) { @SuppressWarnings("unchecked") List<Object> objects = (List<Object>) request.getAttribute(OBJECTS_ATTRIBUTE); if (objects != null) { for (int i = objects.size() - 1; i >= 0; -- i) { Object object = objects.get(i); if (object instanceof Recordable && !State.getInstance(object).isNew()) { return object; } } } return null; } /** * Returns {@code true} if the tool user has requested for inline * editing to be fully enabled. * * @param request Can't be {@code null}. * @return {@code false} if a tool user isn't logged in. */ public static boolean isInlineEditingAllContents(HttpServletRequest request) { if (Settings.isDebug()) { return false; } else { ToolUser user = AuthenticationFilter.Static.getInsecureToolUser(request); return user != null && user.getInlineEditing() == null; } } /** * Gets the view type associated with the given {@code request}. First * checks for the special parameter "_embed" and if set to true will * immediately return {@link #EMBED_VIEW_TYPE}. Next it checks for the * parameter {@link #VIEW_TYPE_PARAMETER} in the request query String, * and then finally falls back to the request attribute * {@link #VIEW_TYPE_ATTRIBUTE}. * * @param request Can't be {@code null}. * @return the view type for the current request. */ static String getViewType(HttpServletRequest request) { String viewType = null; // special case to support module embeds on 3rd party pages // takes precedence over all if (ObjectUtils.to(boolean.class, request.getParameter("_embed"))) { viewType = EMBED_VIEW_TYPE; } // parameter in the request query string has next highest precedence if (StringUtils.isBlank(viewType)) { viewType = request.getParameter(VIEW_TYPE_PARAMETER); } // and finally fall back to the request attribute. if (StringUtils.isBlank(viewType)) { viewType = (String) request.getAttribute(VIEW_TYPE_ATTRIBUTE); } return viewType; } /** * Sets the view type associated with the given {@code request}. Does * nothing if the the view type is already present in the request URL * query string. * * @param request Can't be {@code null}. * @param viewType the view type to set. */ public static void setViewType(HttpServletRequest request, String viewType) { request.setAttribute(VIEW_TYPE_ATTRIBUTE, viewType); } /** @deprecated Use {@link ElFunctionUtils#plainResource} instead. */ @Deprecated public static String getPlainResource(String servletPath) { return ElFunctionUtils.plainResource(servletPath); } /** @deprecated Use {@link ElFunctionUtils#resource} instead. */ @Deprecated public static String getResource(String servletPath) { return ElFunctionUtils.resource(servletPath); } } public static class PathPattern extends Rule { private String pattern; public String getPattern() { return pattern; } public void setPattern(String pattern) { this.pattern = pattern; } // --- Rule support --- @Override public boolean evaluate(Variation variation, Profile profile, Object object) { HttpServletRequest request = PageContextFilter.Static.getRequest(); if (request == null) { return false; } String path = request.getServletPath(); Matcher matcher = Pattern.compile(getPattern()).matcher(path); if (!matcher.matches()) { return false; } List<String> matches = new ArrayList<String>(); StringBuilder pathBuilder = new StringBuilder(); int lastEnd = 0; for (int i = 1, count = matcher.groupCount(); i <= count; ++ i) { matches.add(matcher.group(i)); pathBuilder.append(path.substring(lastEnd, matcher.start(i))); lastEnd = matcher.end(i); } request.setAttribute(PATH_MATCHES_ATTRIBUTE, matches); request.setAttribute("pathMatches", matches); pathBuilder.append(path.substring(lastEnd)); PageFilter.Static.setPath(request, pathBuilder.toString()); return true; } } // --- Deprecated --- /** @deprecated No replacement. */ @Deprecated public static final String EXCEPTION_CSS_INJECTED_ATTRIBUTE = ATTRIBUTE_PREFIX + ".exceptionCssInjected"; /** @deprecated Use {@link Static#getSite} instead. */ @Deprecated public static Site getSite(HttpServletRequest request) { return Static.getSite(request); } /** @deprecated Use {@link Static#setSite} instead. */ @Deprecated public static void setSite(HttpServletRequest request, Site site) { Static.setSite(request, site); } /** @deprecated Use {@link Static#getMainObject} instead. */ @Deprecated public static Object getMainObject(HttpServletRequest request) { return Static.getMainObject(request); } /** @deprecated Use {@link Static#setMainObject} instead. */ @Deprecated public static void setMainObject(HttpServletRequest request, Object mainObject) { Static.setMainObject(request, mainObject); } /** * @deprecated Use {@link Static#getPage} instead. To maintain * backward compatibility, this method will return a * {@code null} instead of looking for the most * appropriate page to render with, if it's called * before {@link #doRequest}. */ @Deprecated public static Page getPage(HttpServletRequest request) { return (Page) request.getAttribute(PAGE_ATTRIBUTE); } /** @deprecated Use {@link Static#setPage} instead. */ @Deprecated public static void setPage(HttpServletRequest request, Page page) { Static.setPage(request, page); } /** @deprecated Use {@link Static#getProfile} instead. */ @Deprecated public static Profile getProfile(HttpServletRequest request) { return Static.getProfile(request); } /** @deprecated Use {@link Static#setProfile} instead. */ @Deprecated public static void setProfile(HttpServletRequest request, Profile profile) { Static.setProfile(request, profile); } /** * @deprecated Use {@link Query#from} and {@link Site#itemsPredicate} * together instead. */ @Deprecated public static <T> Query<T> queryFrom(HttpServletRequest request, Class<T> objectClass) { return Query.from(objectClass).where(Site.OWNER_FIELD + " = ?", getSite(request)); } /** * @deprecated You should let the exception propagate up naturally * instead of catching it and using this method. */ @Deprecated public static void writeException(HttpServletRequest request, Writer writer, Exception exception) throws IOException { if (exception instanceof RuntimeException) { throw (RuntimeException) exception; } else if (exception instanceof IOException) { throw (IOException) exception; } else { throw new RuntimeException(exception); } } /** @deprecated Use {@link Static#getPlainResource} instead. */ @Deprecated public static String getPlainResource(String servletPath) { return Static.getPlainResource(servletPath); } /** @deprecated Use {@link Static#getResource} instead. */ @Deprecated public static String getResource(String servletPath) { return Static.getResource(servletPath); } /** Renders the beginning of the given {@code container}. */ @Deprecated protected static void beginContainer( HttpServletRequest request, HttpServletResponse response, Writer writer, ContainerSection container) throws IOException, ServletException { renderScript(request, response, writer, container.getBeginEngine(), container.getBeginScript()); } /** Renders the end of the given {@code container}. */ @Deprecated protected static void endContainer( HttpServletRequest request, HttpServletResponse response, Writer writer, ContainerSection container) throws IOException, ServletException { renderScript(request, response, writer, container.getEndEngine(), container.getEndScript()); } /** @deprecated No replacement. */ @Deprecated public static final String CURRENT_OBJECT_ATTRIBUTE = ATTRIBUTE_PREFIX + ".currentObject"; /** @deprecated Use {@link Static#peekObject} instead. */ @Deprecated public static Object getCurrentObject(HttpServletRequest request) { return Static.peekObject(request); } /** @deprecated Use {@link Static#pushObject} instead. */ @Deprecated public static void setCurrentObject(HttpServletRequest request, Object object) { request.setAttribute(OBJECTS_ATTRIBUTE, null); Static.pushObject(request, object); } }