/******************************************************************************* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. *******************************************************************************/ package com.ilscipio.scipio.cms.template; import java.io.IOException; import java.io.Serializable; import java.io.StringReader; import java.io.Writer; import java.net.URL; import java.rmi.server.UID; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.ilscipio.scipio.ce.util.SafeOptional; import org.ofbiz.base.util.Debug; import org.ofbiz.base.util.StringUtil; import org.ofbiz.base.util.UtilCodec; import org.ofbiz.base.util.UtilGenerics; import org.ofbiz.base.util.UtilHttp; import org.ofbiz.base.util.UtilMisc; import org.ofbiz.base.util.UtilProperties; import org.ofbiz.base.util.UtilValidate; import org.ofbiz.base.util.collections.MapStack; import org.ofbiz.base.util.collections.ResourceBundleMapWrapper; import org.ofbiz.base.util.string.FlexibleStringExpander; import org.ofbiz.base.util.template.FreeMarkerWorker; import org.ofbiz.base.util.template.ScipioFtlWrappers; import org.ofbiz.entity.GenericEntity; import org.ofbiz.entity.GenericEntityException; import org.ofbiz.entity.transaction.GenericTransactionException; import org.ofbiz.entity.transaction.TransactionUtil; import org.ofbiz.widget.model.ModelScreen; import org.ofbiz.widget.renderer.ScreenRenderer; import org.ofbiz.widget.renderer.ScreenStringRenderer; import org.ofbiz.widget.renderer.macro.MacroScreenRenderer; import org.ofbiz.widget.renderer.macro.MacroScreenViewHandler; import com.ilscipio.scipio.cms.CmsException; import com.ilscipio.scipio.cms.content.CmsPage; import com.ilscipio.scipio.cms.content.CmsPageContent; import com.ilscipio.scipio.cms.content.CmsPageContext; import com.ilscipio.scipio.cms.control.CmsControlState; import com.ilscipio.scipio.cms.data.Preloadable; import com.ilscipio.scipio.cms.data.Preloadable.AbstractPreloadable; import freemarker.core.Environment; import freemarker.core.Environment.Namespace; import freemarker.ext.beans.BeansWrapper; import freemarker.template.Configuration; import freemarker.template.Template; /** * Interface for any template meant to be rendered to an output (in contrast * to templates such as script templates that execute instead of rendering). */ public interface CmsRenderTemplate extends Serializable { public TemplateRenderer<?> getRenderer(); /** * Template renderer nested (or inner) class base. * <p> * (workaround for multiple inheritance limitations). */ @SuppressWarnings("serial") public static abstract class TemplateRenderer<T extends CmsComplexTemplate & CmsRenderTemplate> extends AbstractPreloadable implements Preloadable, Serializable { private static final Debug.OfbizLogger module = Debug.getOfbizLogger(java.lang.invoke.MethodHandles.lookup().lookupClass()); // 2017-02: this is now unacceptable: we must use dynamically built one that can // depend on theme selection. // protected static final ScreenStringRenderer screenStringRenderer; // static { // ScreenStringRenderer renderer = null; // try { // renderer = new MacroScreenRenderer(UtilProperties.getPropertyValue("widget", "screen.name"), UtilProperties.getPropertyValue("widget", "screen.screenrenderer")); // } catch (Throwable t) { // Debug.logError(t, module); // } // screenStringRenderer = renderer; // } protected static final Configuration fmConfig = makeConfiguration((BeansWrapper) ScipioFtlWrappers.getSystemObjectWrapperFactory().getExtendedWrapper(FreeMarkerWorker.version, "html")); public static final Set<String> cmsOfbizCtxSysVarNames = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(new String [] { // stock ofbiz+scipio names "screens", "globalContext", "nullField", "parameters", "delegator", "dispatcher", "security", "locale", "userLogin", "nowTimestamp", "delegator", "dispatcher", "security", "org.apache.catalina.jsp_classpath", "autoUserLogin", "userLogin", "timeZone", "request", "response", "session", "application", "javaScriptEnabled", "sessionAttributes", "requestAttributes", "JspTaglibs", "requestParameters", "Application", "Request", "checkLoginUrl", "externalLoginKey", "externalKeyParam", "eventMessageList", "errorMessageList", "serviceValidationException", "isError", "requestMethod", }))); public static final Set<String> cmsNewCtxSysVarNames = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(new String [] { // new cms names "cmsCtxSysVarNames", "cmsPageContext", "cmsPageContent", "cmsContent", "cmsPageTemplate", "cmsPage", "cmsPageId", "cmsIsPreview", CmsControlState.ATTR }))); // FIXME: 2016: we need to unhardcode this names list through an Ofbiz patch. // for now just hardcode because it's the best way unfortunately... // NOTE: this is NOT all... omits very reusable names like "partyGroup" and "person"... public static final Set<String> cmsCtxSysVarNames; static { Set<String> names = new HashSet<>(); names.addAll(cmsOfbizCtxSysVarNames); names.addAll(cmsNewCtxSysVarNames); cmsCtxSysVarNames = Collections.unmodifiableSet(names); } // NOTE: Template not serializable! protected transient SafeOptional<Template> fmTemplate = null; // NOTE: 2016: Optional is required for thread safety (preload) protected final T template; public TemplateRenderer(T template) { this.template = template; } /** * 2016: Loads ALL this object's content into the current instance. * <p> * WARN: IMPORTANT: AFTER THIS CALL, * NO FURTHER CALLS ARE ALLOWED TO MODIFY THE INSTANCE IN MEMORY. * Essential for thread safety!!! */ @Override public void preload(PreloadWorker preloadWorker) { super.preload(preloadWorker); // NOTE: the Freemarker template can't really be made explicitly immutable, but it should be thread-safe anyway. this.getFreeMarkerTemplate(); } public static Configuration makeConfiguration(BeansWrapper wrapper) { Configuration cfg = FreeMarkerWorker.makeConfiguration(wrapper); cfg.setTemplateExceptionHandler(new CmsRenderUtil.CmsTemplateExceptionHandler()); // CMS-specific imports for(Map.Entry<Object, Object> entry : UtilProperties.getProperties("cmsFreemarkerImports").entrySet()) { cfg.addAutoImport((String) entry.getKey(), (String) entry.getValue()); } // CMS-specific includes for(Map.Entry<String, String> entry : UtilProperties.asSortedMap(UtilProperties.getProperties("cmsFreemarkerIncludes")).entrySet()) { cfg.addAutoInclude((String) entry.getValue()); } // CMS-specific directives // SCIPIO: TODO: delegate to FreeMarkerWorker and remove license notice // Transforms properties file set up as key=transform name, property=transform class name ClassLoader loader = Thread.currentThread().getContextClassLoader(); Enumeration<URL> resources; try { resources = loader.getResources("cmsFreemarkerTransforms.properties"); } catch (IOException e) { Debug.logError(e, "Could not load list of cmsFreemarkerTransforms", module); throw UtilMisc.initCause(new InternalError(e.getMessage()), e); } while (resources.hasMoreElements()) { URL propertyURL = resources.nextElement(); Debug.logInfo("loading properties: " + propertyURL, module); Properties props = UtilProperties.getProperties(propertyURL); if (UtilValidate.isEmpty(props)) { Debug.logError("Unable to locate properties file " + propertyURL, module); } else { loadTransforms(loader, props, cfg); } } return cfg; } public static Configuration getDefaultCmsConfig() { return fmConfig; } /** * Protected helper method. * <p> * SCIPIO: TODO: delegate to FreeMarkerWorker and remove license notice */ protected static void loadTransforms(ClassLoader loader, Properties props, Configuration config) { for (Iterator<Object> i = props.keySet().iterator(); i.hasNext();) { String key = (String) i.next(); String className = props.getProperty(key); if (Debug.verboseOn()) { Debug.logVerbose("Adding FTL Transform " + key + " with class " + className, module); } try { config.setSharedVariable(key, FreeMarkerWorker.getTransformInstance(className, loader)); } catch (Exception e) { Debug.logError(e, "Could not pre-initialize dynamically loaded class: " + className + ": " + e, module); } } } public T getCmsTemplate() { return template; } public Template getTemplate() { return getFreeMarkerTemplate(); } protected Template getFreeMarkerTemplate() { SafeOptional<Template> fmTemplate = this.fmTemplate; if (fmTemplate == null) { try { // UID trick, same as used by widgets to generate unique names String name = template.getName() + "_" + new UID().toString(); fmTemplate = SafeOptional.ofNullable(new Template(name, new StringReader(template.getTemplateBody()), fmConfig)); } catch (IOException e) { throw new CmsException("Freemarker template could not be retrieved from database", e); } this.fmTemplate = fmTemplate; } return fmTemplate.orElse(null); } // TODO?: clean up this class, constructors hard to follow (booleans) public static class RenderArgs { private Writer out; private Environment env; // NOTE: optional for standalone render (uses its own environment) private MapStack<String> context; private CmsPageContent content; private CmsPageContext pageContext; private Map<String, Object> earlyCtxVars; // early ctx vars, similar to ofbiz widget-style context passing private Map<String, Object> ovrdCtxVars; // attribute-overriding ctx vars private boolean skipSystemCtx; private boolean systemCtxCmsOnly = false; private boolean protectScopeSystem = true; private boolean skipExtraCommonCtx; private boolean protectScope; private boolean newCmsCtx = true; // high-level flag mainly used by subclasses private FlexibleStringExpander txTimeoutExdr; public RenderArgs() { this.skipSystemCtx = false; this.skipExtraCommonCtx = false; this.protectScope = true; } public RenderArgs(Writer out, Environment env, MapStack<String> context, CmsPageContent content, CmsPageContext pageContext, Map<String, Object> earlyCtxVars, Map<String, Object> ovrdCtxVars, boolean skipSystemCtx, boolean skipExtraCommonCtx, boolean protectScope) { this.out = out; setEnv(env); this.env = env; this.context = context; this.content = content; this.pageContext = pageContext; this.earlyCtxVars = earlyCtxVars; this.ovrdCtxVars = ovrdCtxVars; this.skipSystemCtx = skipSystemCtx; this.skipExtraCommonCtx = skipExtraCommonCtx; this.protectScope = protectScope; } public RenderArgs(Writer out, MapStack<String> context, CmsPageContent content, CmsPageContext pageContext, Map<String, Object> earlyCtxVars, Map<String, Object> ovrdCtxVars, boolean protectScope) { this(out, null, context, content, pageContext, earlyCtxVars, ovrdCtxVars, false, false, protectScope); } public Writer getOut() { return out; } public void setOut(Writer out) { this.out = out; } public Environment getEnv() { return env; } public void setEnv(Environment env) { this.env = env; if (out == null && env != null) { this.out = env.getOut(); } } public MapStack<String> getContext() { return context; } public void setContext(MapStack<String> context) { this.context = context; } public CmsPageContent getContent() { return content; } public void setContent(CmsPageContent content) { this.content = content; } public CmsPageContext getPageContext() { return pageContext; } public void setPageContext(CmsPageContext pageContext) { this.pageContext = pageContext; } public Map<String, Object> getEarlyCtxVars() { return earlyCtxVars; } public void setEarlyCtxVars(Map<String, Object> earlyCtxVars) { this.earlyCtxVars = earlyCtxVars; } public Map<String, Object> getOvrdCtxVars() { return ovrdCtxVars; } public void setOvrdCtxVars(Map<String, Object> lateCtxVars) { this.ovrdCtxVars = lateCtxVars; } public boolean isSkipSystemCtx() { return skipSystemCtx; } public void setSkipSystemCtx(boolean skipSystemCtx) { this.skipSystemCtx = skipSystemCtx; } public boolean isSystemCtxCmsOnly() { return systemCtxCmsOnly; } public void setSystemCtxCmsOnly(boolean systemCtxCmsOnly) { this.systemCtxCmsOnly = systemCtxCmsOnly; } public boolean isProtectScopeSystem() { return protectScopeSystem; } public void setProtectScopeSystem(boolean protectScopeSystem) { this.protectScopeSystem = protectScopeSystem; } public boolean isSkipExtraCommonCtx() { return skipExtraCommonCtx; } public void setSkipExtraCommonCtx(boolean skipExtraCommonCtx) { this.skipExtraCommonCtx = skipExtraCommonCtx; } public boolean isProtectScope() { return protectScope; } public void setProtectScope(boolean protectScope) { this.protectScope = protectScope; } public CmsPage getPage() { return (content != null) ? content.getPage() : null; } // workaround for lack of CmsPage property public boolean isNewCmsCtx() { return newCmsCtx; } public void setNewCmsCtx(boolean newCmsCtx) { this.newCmsCtx = newCmsCtx; } public FlexibleStringExpander getTxTimeoutExdr() { return txTimeoutExdr; } public void setTxTimeoutExdr(FlexibleStringExpander txTimeout) { this.txTimeoutExdr = txTimeout; } public boolean hasTxTimeout() { return (txTimeoutExdr != null && !txTimeoutExdr.isEmpty()); } public boolean isTxEnabled() { // NOTE: This is a pre-expansion check return !(txTimeoutExdr != null && "0".equals(txTimeoutExdr.getOriginal())); } } /** * Merges the content with the template with consideration to the given * context information and renders to the passed writer. */ public Object processAndRender(RenderArgs renderArgs) throws CmsException { boolean useTransaction = renderArgs.isTxEnabled(); // NOTE: If txTimeout is exactly the string "0", prevents transaction, even if req attr was set boolean beganTransaction = false; final boolean protectScope = !renderArgs.isProtectScope(); final int origStackSize = renderArgs.getContext().stackSize(); if (protectScope) { renderArgs.getContext().push(); } try { // Populate context if (renderArgs.getContent().isImmutable()) { // this makes the error clearer throw new IllegalStateException("Tried to populate CMS page directives on immutable CmsPageContent"); } boolean protectScopeSystem = false; try { // TODO: REVIEW: At current time (2019-02), we will have to emulate ModelScreen and start the // transaction after system context is populated but BEFORE screen scripts are run, // otherwise the txTimeout expression has no context to work with. if (!renderArgs.isSkipSystemCtx()) { populateSystemRenderContext(renderArgs); if (renderArgs.isProtectScopeSystem()) { renderArgs.getContext().push(); protectScopeSystem = true; } } else { // Emulates ModelScreen behavior... renderArgs.getContext().put("nullField", GenericEntity.NULL_FIELD); } } catch (Throwable t) { throw new CmsException("Error preparing context for template render: " + t.getMessage(), t); } try { try { // TODO: REVIEW: For now, leaving populateProcessorScript BEFORE transaction start, because otherwise // it will be inconsistent with the scripts invoked from populateSystemRenderContext, AND // it's possible the txTimeout expander needs to fetch something from the common processor. // We COULD split populateExtraCommonRenderContext into 2 and include the processor in the transaction, // but for the time being, we don't even supply a processor anymore, so it's unclear if there's any real need // for it to be wrapped in a transaction. if (!renderArgs.isSkipExtraCommonCtx()) { populateExtraCommonRenderContext(renderArgs); } if (useTransaction) { beganTransaction = beginRenderTx(renderArgs); } populateWidgetRenderContext(renderArgs); } catch (Throwable t) { throw new CmsException("Error preparing context for template render: " + t.getMessage(), t); } // Render template renderTemplate(renderArgs.getOut(), renderArgs.getContext()); } finally { if (protectScopeSystem) { renderArgs.getContext().pop(); } } TransactionUtil.commit(beganTransaction); return null; } catch (Exception e) { // Implied: | CmsException e) String errMsg = "Error rendering CMS template: " + e.toString(); try { TransactionUtil.rollback(beganTransaction, errMsg, e); } catch (GenericEntityException e2) { Debug.logError("Cms: Could not rollback transaction: " + e2.toString(), module); } if (e instanceof CmsException) { throw (CmsException) e; } else if (e instanceof RuntimeException) { throw (RuntimeException) e; // TODO: REVIEW: Should this get wrapped in a CmsException or not? } else { throw new CmsException(e); } } finally { if (protectScope) { renderArgs.getContext().pop(); } if (renderArgs.getContext().stackSize() > origStackSize) { Debug.logWarning("Cms: Unmatched push() calls at render end: stack size (" + renderArgs.getContext().stackSize() + ") is greater than expected (" + origStackSize + ")", module); } } } protected static boolean beginRenderTx(RenderArgs renderArgs) throws GenericTransactionException { Integer transactionTimeout = getRenderTxTimeout(renderArgs); // NOTE: Value zero (0) is the same as useTransaction==false if (transactionTimeout == null || transactionTimeout < 0) { return TransactionUtil.begin(); } else if (transactionTimeout > 0) { return TransactionUtil.begin(transactionTimeout); } return false; } protected static Integer getRenderTxTimeout(RenderArgs renderArgs) { Integer transactionTimeout = null; if (!TransactionUtil.isTransactionInPlaceSafe()) { // TODO: Investigate if it's worth unhardcoding transactionTimeoutAttr and a transactionTimeoutParam like widgets final String transactionTimeoutAttr = ModelScreen.TRANSACTION_TIMEOUT_ATTR; Object transactionTimeoutObj = null; // Check the "TRANSACTION_TIMEOUT" request attribute for consistency/compat with widget renderer (ModelScreen) HttpServletRequest request = UtilGenerics.cast(renderArgs.getContext().get("request")); if (request != null) { transactionTimeoutObj = UtilHttp.getAllAttr(request, ModelScreen.TRANSACTION_TIMEOUT_ATTR); } if (transactionTimeoutObj != null) { try { transactionTimeout = ModelScreen.parseTransactionTimeout(transactionTimeoutObj); } catch (NumberFormatException e) { String msg = "Cms: Transaction timeout attr/param '" + transactionTimeoutAttr + "' is invalid and" + "it will be ignored: " + e.toString(); Debug.logWarning(msg, module); } } if (transactionTimeout == null && renderArgs.hasTxTimeout()) { transactionTimeoutObj = renderArgs.getTxTimeoutExdr().expand(renderArgs.getContext()); try { transactionTimeout = ModelScreen.parseTransactionTimeout(transactionTimeoutObj); } catch (NumberFormatException e) { Debug.logWarning(e, "Cms: Could not parse transaction-timeout value, original=[" + renderArgs.getTxTimeoutExdr() + "], expanded=[" + transactionTimeoutObj + "]", module); } } } return transactionTimeout; } /** * Invokes the actual template rendering as a standalone template. * Assumes context already prepared. */ public void renderTemplate(Writer out, Map<String, Object> context) throws CmsException { try { Template template = getFreeMarkerTemplate(); // 2017-03-20: MUST USE FreeMarkerWorker due to possible environment snafus... //template.process(context, out); FreeMarkerWorker.renderTemplate(template, context, out); } catch (Throwable t) { throw new CmsException("Error rendering template: " + t.getMessage(), t); } } /** * Includes the template as if Freemarker <code>#include</code> directive had been used, * without running any context population. */ public void includeTemplate(Environment env) throws CmsException { try { Template template = getFreeMarkerTemplate(); env.include(template); } catch (Throwable t) { throw new CmsException("Error rendering template: " + t.getMessage(), t); } } /** * Imports the template as if Freemarker <code>#import</code> directive had been used, * without running any context population. */ public Namespace importTemplate(Environment env, String namespace) throws CmsException { try { Template template = getFreeMarkerTemplate(); return env.importLib(template, namespace); } catch (Throwable t) { throw new CmsException("Error rendering template: " + t.getMessage(), t); } } /** * Populates the core system render context, including new CMS objects considered necessary * to the core. * <p> * Includes all context population duties normally done in widget renderer * by MacroScreenViewHandler.render, ScreenRenderer.populateContextForRequest, and * CMS-specific additions. * <p> * Does NOT contain extras such as common UI label maps - see populateExtraCmsSystemRenderContext. */ protected void populateSystemRenderContext(RenderArgs renderArgs) { if (renderArgs.isSystemCtxCmsOnly()) { populateContextForRequestCmsOnly(renderArgs.getContext(), null, renderArgs.getContent(), renderArgs.getPageContext(), getRenderPageTemplate(renderArgs)); } else { populateContextForRequest(renderArgs.getContext(), null, renderArgs.getContent(), renderArgs.getPageContext(), getRenderPageTemplate(renderArgs)); } if (!renderArgs.isSystemCtxCmsOnly()) { populateSystemViewHandlerRenderContext(renderArgs.getContext(), renderArgs.getPageContext().getRequest(), renderArgs.getPageContext().getResponse(), renderArgs.getOut()); } } /** * Populates the system context with the extra context fields normally added by * {@link org.ofbiz.widget.renderer.macro.MacroScreenViewHandler#render}, * which are not covered by {@link org.ofbiz.widget.renderer.ScreenRenderer#populateContextForRequest} */ public static void populateSystemViewHandlerRenderContext(MapStack<String> context, HttpServletRequest request, HttpServletResponse response, Writer writer) { // NOTE: 2017-02: we must emulate MacroScreenViewHandler and use dynamic renderer here, // because of theme-switchable support. // DEV NOTE: IMPORTANT: keep this method in sync with the context-populate parts of // MacroScreenViewHandler.render final String screenRendererName = "screen"; ScreenStringRenderer screenStringRenderer; try { screenStringRenderer = MacroScreenViewHandler.loadRenderers(request, response, screenRendererName, context, writer); } catch (Exception e) { throw new CmsException(e); } ScreenRenderer screens = ScreenRenderer.makeWithEnvAwareFetching(writer, context, screenStringRenderer); context.put("screens", screens); // SCIPIO: 2016-09-15: in addition, dump the screens renderer into the request attributes, // for some cases where only request is available request.setAttribute("screens", screens); // SCIPIO: new early encoder UtilCodec.SimpleEncoder simpleEncoder = UtilCodec.getEncoder(UtilProperties.getPropertyValue("widget", screenRendererName + ".encoder")); context.put("simpleEncoder", simpleEncoder); UtilCodec.SimpleEncoder simpleEarlyEncoder = UtilCodec.getEncoder(UtilProperties.getPropertyValue("widget", screenRendererName + ".earlyEncoder")); context.put("simpleEarlyEncoder", (simpleEarlyEncoder != null) ? simpleEarlyEncoder : simpleEncoder); try { // SPECIAL: 2017-03-09: must register initial context manually here because renderScreenBegin never called // TODO?: registerContext should be a ScreenStringRenderer method (no cast) ((MacroScreenRenderer) screenStringRenderer).registerContext(writer, context); } catch (Exception e) { throw new CmsException(e); } } protected CmsPageTemplate getRenderPageTemplate(RenderArgs renderArgs) { if (this.template instanceof CmsPageTemplate) { return (CmsPageTemplate) this.template; } else { // here, probably rendering asset... return null; } } /** * Populates context for a new request. * <p> * Based on {@link org.ofbiz.widget.renderer.ScreenRenderer#populateContextForRequest}. * NOTE: 2019-06-03: Unlike ScreenRenderer, this does <b>not</b> protect scope - caller must do it as needed. * <p> * DEV NOTE: MUST BE MAINTAINED TO MATCH <code>ScreenRenderer.populateContextForRequest</code>. * Try to keep as close as possible in every way to that method (down to code style), * to minimize chances of missing anything. */ public static void populateContextForRequest(MapStack<String> context, ScreenRenderer screens, CmsPageContent pageContent, CmsPageContext pageContext, CmsPageTemplate pageTemplate) { // top request-/page-level cms-specific variables populateTopCmsContextVariables(context, pageContent, pageContext, pageTemplate); ScreenRenderer.populateContextForRequest(context, false, screens, pageContext.getRequest(), pageContext.getResponse(), pageContext.getServletContext()); } public static void populateContextForRequestCmsOnly(MapStack<String> context, ScreenRenderer screens, CmsPageContent pageContent, CmsPageContext pageContext, CmsPageTemplate pageTemplate) { populateTopCmsContextVariables(context, pageContent, pageContext, pageTemplate); } public static void populateTopCmsContextVariables(MapStack<String> context, CmsPageContent pageContent, CmsPageContext pageContext, CmsPageTemplate pageTemplate) { context.put("cmsPageContext", pageContext); context.put("cmsPageContent", pageContent); context.put("cmsContent", pageContent); // NOTE: this one gets overridden after, but also here just in case context.put("cmsPageTemplate", pageTemplate); CmsPage page = (pageContent != null) ? pageContent.getPage() : null; context.put("cmsPage", page); // NOTE: mainly for debugging and such... context.put("cmsPageId", (page != null) ? page.getId() : null); // NOTE: mainly for debugging and such... context.put("cmsIsPreview", pageContext.isPreview()); context.put("cmsCtxSysVarNames", cmsCtxSysVarNames); context.put(CmsControlState.ATTR, pageContext.getControlState()); } /** * Extra CMS-specific common context prep, that for practical purposes may be considered as part of system context setup, * although could also not be considered as such, and is NOT included as part of the system context stack push. */ protected void populateExtraCommonRenderContext(RenderArgs renderArgs) { // 2017-02-20: MOVED these executions here, as part of "system", because it makes them easier to understand this way. Set<String> contextSkipNames = CmsRenderUtil.getContextSystemVarNamesAlways(renderArgs.getContext()); populateDefaultLabelMaps(renderArgs, contextSkipNames); // NOTE: 2017: we no longer run processor script at every asset invocation. only top page template does this. lessened in importance anyway. populateProcessorScript(renderArgs, contextSkipNames); } /** * Populates widget-level context, or in other words things that would be found * in actions section of a screen widget, such as scripts and page context. * <p> * NOTE: widget is loose term; no relation to ofbiz widgets. * <p> * Subclass <em>could</em> need to override this. */ protected void populateWidgetRenderContext(RenderArgs renderArgs) { // for assets, most of the scripts and page-level content should already be in context // at this point. we only need to do the asset content. Set<String> contextSkipNames = CmsRenderUtil.getContextSystemVarNamesAlways(renderArgs.getContext()); populateScriptsAndContent(renderArgs, contextSkipNames); } protected void populateEarlyExtraCtxVars(RenderArgs renderArgs) { if (renderArgs.getEarlyCtxVars() != null) { renderArgs.getContext().putAll(renderArgs.getEarlyCtxVars()); } } protected void populateOvrdExtraCtxVars(RenderArgs renderArgs) { if (renderArgs.getOvrdCtxVars() != null) { // apply and consume renderArgs.getContext().putAll(renderArgs.getOvrdCtxVars()); } } /** * TODO?: REMOVE? this should be covered by the templates themselves, and forcing this * might introduce ordering issues. */ protected void populateDefaultLabelMaps(RenderArgs renderArgs, Set<String> contextSkipNames) { /* Add uiLabelMaps (this is screen-/page-specific, may be overwritten, so after push)*/ try{ ResourceBundleMapWrapper uiLabelMap = UtilProperties.getResourceBundleMap("CommonUiLabels", UtilHttp.getLocale((HttpServletRequest) renderArgs.getContext().get("request"))); renderArgs.getContext().put("uiLabelMap", uiLabelMap); } catch(Exception e) { Debug.logError(e, "Cms: Error loading CommonUiLabels: " + e.getMessage(), module); } } /** * @deprecated too simplistic - no expansion timing control. */ @Deprecated protected void populateInitialPageContent(RenderArgs renderArgs, Set<String> contextSkipNames) { CmsPageContent content = renderArgs.getContent().normalizeForAttributes(template.getExpansionSortedAttributeTemplates(), renderArgs.getPageContext()); content.transferToContext(renderArgs.getContext(), contextSkipNames); setUpdatePageContentInstance(renderArgs, content); } /** * @deprecated too simplistic - no expansion timing control. */ @Deprecated protected void setUpdatePageContentInstance(RenderArgs renderArgs, CmsPageContent content) { renderArgs.setContent(content); // theoreticially needed, but in practice usually will end up being same instance... renderArgs.getContext().put("cmsContent", content); } /** * @deprecated too simplistic - no expansion timing control. */ @Deprecated protected void populateExpandedPageContent(RenderArgs renderArgs, Set<String> contextSkipNames) { // NOTE: 2017: the source injection context is simply the rendering context. it already contains everything needed, can avoid more copies. // NOTE: injectVariableContent currently injects/replaces content in-place (into first map) CmsPageContent content = renderArgs.getContent().parseExpandAttributes(template.getExpansionSortedAttributeTemplates(), renderArgs.getContext(), renderArgs.getPageContext()); // Save all updated (injected) content back into context (making sure not to override important names) content.transferToContext(renderArgs.getContext(), contextSkipNames); setUpdatePageContentInstance(renderArgs, content); } /** * NOTE: 2017: the CMS-specific processor script is no longer of major importance in Scipio, * because Scipio supports custom system-wide and webapp-specific global scripts. * It is preferable to reuse those mechanisms, unless the script is truly CMS-specific. */ protected void populateProcessorScript(RenderArgs renderArgs, Set<String> contextSkipNames) { try { CmsScriptTemplate.getProcessorScriptExecutor().execute(renderArgs.getContext()); } catch (Exception e) { throw new CmsException(e); } } /** * NOTE: does not do attributes/content, only use this if not using those at all. * @see #populateScriptsAndContent */ protected void populateScripts(RenderArgs renderArgs, Set<String> contextSkipNames) { populateScripts(getSortedScriptTemplates(), renderArgs, contextSkipNames); } protected void populateScripts(List<CmsScriptTemplate> scriptTemplates, RenderArgs renderArgs, Set<String> contextSkipNames) { if (scriptTemplates != null) { for (CmsScriptTemplate scriptTemplate : scriptTemplates) { populateScript(scriptTemplate, renderArgs, contextSkipNames); } } } protected void populateScript(CmsScriptTemplate scriptTemplate, RenderArgs renderArgs, Set<String> contextSkipNames) { try { scriptTemplate.getExecutor().execute(renderArgs.getContext()); } catch (Exception e) { throw new CmsException(e); } } /** * Common script + attribute population code common to both assets and pages. * Does not include script processor. * <p> * this will execute attributes and scripts in the precise right order, and also run * the "last" extra context vars, which are set after the last attributes (to * allow overriding them). * <p> * NOTE: 2017: this rewrites the old PageContent logic so that instead of dumping the page content * contexts blindly with putAll, we handle each CmsAttributeTemplate explicitly. * this also fixes the problem of unspecified attributes receiving arbitrary values through * the inherited context. * this also means we ditch any page content that does not apply to the exact attribute * definitions we are using, we never get any leftover values from prior definitions * and we prevent undefined and arbitrary behavior. */ protected void populateScriptsAndContent(RenderArgs renderArgs, Set<String> contextSkipNames) { // old code order (too simplistic) // populateEarlyExtraCtxVars(renderArgs); // populateInitialPageContent(renderArgs, contextSkipNames); // populateExpandedPageContent(renderArgs, contextSkipNames); // populateLateExtraCtxVars(renderArgs); // populateScripts(renderArgs, contextSkipNames); // read attribs and scripts List<CmsAttributeTemplate> attribs = orEmpty(getSortedAttributeTemplates()); Iterator<CmsAttributeTemplate> attribIt = attribs.iterator(); List<CmsScriptTemplate> scripts = orEmpty(getSortedScriptTemplates()); Iterator<CmsScriptTemplate> scriptIt = scripts.iterator(); // populate early context vars (usually passed through macro call such as @asset ctxVars={...}) populateEarlyExtraCtxVars(renderArgs); // TODO: review if needed, but seeing no need for this since handling attributes explicitly. //renderArgs.getContent().normalizeForAttributes(template.getExpansionSortedAttributeTemplates(), renderArgs.getPageContext()); // initial populate late context vars (usually passed through macro call such as @asset ovrdCtxVars={...}) // NOTE: "overriding/late" extra context vars: these are meant to override attributes. // but there may be key names that don't match any attributes; those ones we will // simply set immediately. // TODO: REVIEW: to simplify, for now just dump all the late vars, basically twice for // those that match attribs. populateOvrdExtraCtxVars(renderArgs); // Expand & transfer attributes to context, and run scripts, in the global order // defined by CmsAttributeTemplate.expandPosition together with CmsScriptTemplate.inputPosition. CmsAttributeTemplate attrib = attribIt.hasNext() ? attribIt.next() : null; CmsScriptTemplate script = scriptIt.hasNext() ? scriptIt.next() : null; while(attrib != null && script != null) { if (attrib.expandsBefore(script)) { populateAttributeOrOvrd(attrib, renderArgs, contextSkipNames); attrib = attribIt.hasNext() ? attribIt.next() : null; } else { populateScript(script, renderArgs, contextSkipNames); script = scriptIt.hasNext() ? scriptIt.next() : null; } } while(attrib != null) { populateAttributeOrOvrd(attrib, renderArgs, contextSkipNames); attrib = attribIt.hasNext() ? attribIt.next() : null; } while(script != null) { populateScript(script, renderArgs, contextSkipNames); script = scriptIt.hasNext() ? scriptIt.next() : null; } } /** * Expands (if applicable) the attribute and transfers it into context. If an ovrdCtxVar exists * for the same name, it replaces the attribute instead. */ protected void populateAttributeOrOvrd(CmsAttributeTemplate attributeTemplate, RenderArgs renderArgs, Set<String> contextSkipNames) { String name = attributeTemplate.getName(); if (contextSkipNames.contains(name)) { // this is not always fatal or a problem, but can cause serious issues in some cases Debug.logWarning("Cms: Attribute template '" + name + "' (id: " + attributeTemplate.getId() + ") uses the name of a reserved system context variable (consider renaming)", module); } if (renderArgs.getOvrdCtxVars() != null && renderArgs.getOvrdCtxVars().containsKey(name)) { // overriding var def // NOTE: this should already be in context due to populateOvrdExtraCtxVars call, // but put it again to be safe. renderArgs.getContext().put(name, renderArgs.getOvrdCtxVars().get(name)); } else { // parse and expand the attribute. // NOTE: this both re-stores it in the CmsPageContent (cmsContent) // and in the top-level context. Object value = renderArgs.getContent().parseExpandAttribute(attributeTemplate, renderArgs.getContext(), renderArgs.getPageContext(), true); attributeTemplate.getInheritMode().setValue(renderArgs.getContext(), renderArgs.getContent(), attributeTemplate, attributeTemplate.getName(), value); } } private static <T> List<T> orEmpty(List<T> list) { return list != null ? list : Collections.<T> emptyList(); } protected List<CmsScriptTemplate> getSortedScriptTemplates() { // sub-classes should override. return null; } protected List<CmsAttributeTemplate> getSortedAttributeTemplates() { return template.getExpansionSortedAttributeTemplates(); } /** * Sets the standard Context variables dynamically * TODO: Check whether or not this is still being used - if it is, the returned object doesn't seem to be the correct one. * I think scriptContext must be added back to content somehow for it to work... */ public CmsPageContent populateBasicContextVariables(CmsPageContent content, CmsPageContext pageContext) { try { Map<String, Object> scriptContext = new HashMap<>(); HttpServletRequest request = pageContext.getRequest(); HttpServletResponse response = pageContext.getResponse(); scriptContext.put("request", request); scriptContext.put("response", response); HttpSession session = request.getSession(); scriptContext.put("session", session); scriptContext.put("dispatcher", request.getAttribute("dispatcher")); scriptContext.put("delegator", request.getAttribute("delegator")); scriptContext.put("security", request.getAttribute("security")); scriptContext.put("locale", UtilHttp.getLocale(request)); scriptContext.put("timeZone", UtilHttp.getTimeZone(request)); scriptContext.put("userLogin", session.getAttribute("userLogin")); scriptContext.put("parameters", UtilHttp.getCombinedMap(request, UtilMisc.toSet("delegator", "dispatcher", "security", "locale", "timeZone", "userLogin"))); // make sure the "nullField" object is in there for entity ops; note this is nullField and not null because as null causes problems in FreeMarker and such... scriptContext.put("nullField", GenericEntity.NULL_FIELD); scriptContext.put("StringUtil", StringUtil.INSTANCE); scriptContext.put("cmsPageContext", pageContext); scriptContext.put("cmsContent",content); CmsScriptTemplate.getProcessorScriptExecutor().executeSafe(scriptContext); } catch (Exception e) { Debug.logError(e, module); } return content; } } }