You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ofbiz.apache.org by ja...@apache.org on 2020/05/13 12:19:54 UTC

[ofbiz-framework] branch trunk updated: Improved: widget tag (OFBIZ-11686)

This is an automated email from the ASF dual-hosted git repository.

jamesyong pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 803f7fa  Improved: <script-template> widget tag (OFBIZ-11686)
803f7fa is described below

commit 803f7fa06048baa5c4246c80ff249cba2edb9451
Author: James Yong <ja...@apache.org>
AuthorDate: Wed May 13 20:19:05 2020 +0800

    Improved: <script-template> widget tag (OFBIZ-11686)
    
    The new tag allows us to render a freemarker template containing javascript,
    as external script in html instead of inline script.
    This helps to reduce CSP errors.
---
 applications/order/template/order/FindOrders.ftl   | 74 -----------------
 .../order/template/order/FindOrders.js.ftl         | 86 ++++++++++++++++++++
 .../order/widget/ordermgr/OrderViewScreens.xml     |  5 +-
 .../java/org/apache/ofbiz/common/CommonEvents.java | 25 ++++++
 .../common/webcommon/WEB-INF/common-controller.xml | 11 +++
 .../webapp/ftl/ScriptTemplateListTransform.java    | 81 +++++++++++++++++++
 .../ofbiz/webapp/freemarkerTransforms.properties   |  1 +
 framework/widget/dtd/widget-screen.xsd             |  8 ++
 .../widget/artifact/ArtifactInfoGatherer.java      |  5 ++
 .../org/apache/ofbiz/widget/model/HtmlWidget.java  | 72 +++++++++++++++++
 .../ofbiz/widget/model/ModelWidgetVisitor.java     |  2 +
 .../ofbiz/widget/model/ScriptTemplateUtil.java     | 92 ++++++++++++++++++++++
 .../ofbiz/widget/model/XmlWidgetVisitor.java       |  9 +++
 .../widget/renderer/html/HtmlWidgetRenderer.java   |  4 +
 themes/flatgrey/template/Footer.ftl                |  1 +
 themes/rainbowstone/template/includes/Footer.ftl   |  1 +
 themes/tomahawk/template/Footer.ftl                |  1 +
 17 files changed, 403 insertions(+), 75 deletions(-)

diff --git a/applications/order/template/order/FindOrders.ftl b/applications/order/template/order/FindOrders.ftl
index cb2c338..0d1923d 100644
--- a/applications/order/template/order/FindOrders.ftl
+++ b/applications/order/template/order/FindOrders.ftl
@@ -17,80 +17,6 @@ specific language governing permissions and limitations
 under the License.
 -->
 
-<script type="application/javascript">
-<!-- //
-function lookupOrders(click) {
-    orderIdValue = document.lookuporder.orderId.value;
-    if (orderIdValue.length > 1) {
-        document.lookuporder.action = "<@o...@ofbizUrl>";
-        document.lookuporder.method = "get";
-    } else {
-        document.lookuporder.action = "<@o...@ofbizUrl>";
-    }
-
-    if (click) {
-        document.lookuporder.submit();
-    }
-    return true;
-}
-function toggleOrderId(master) {
-    var form = document.massOrderChangeForm;
-    var orders = form.elements.length;
-    for (var i = 0; i < orders; i++) {
-        var element = form.elements[i];
-        if ("orderIdList" == element.name) {
-            element.checked = master.checked;
-        }
-    }
-    toggleOrderIdList();
-}
-function setServiceName(selection) {
-    document.massOrderChangeForm.action = selection.value;
-}
-function runAction() {
-    var form = document.massOrderChangeForm;
-    form.submit();
-}
-
-function toggleOrderIdList() {
-    var form = document.massOrderChangeForm;
-    var orders = form.elements.length;
-    var isAllSelected = true;
-    var isSingle = true;
-    for (var i = 0; i < orders; i++) {
-        var element = form.elements[i];
-        if ("orderIdList" == element.name) {
-            if (element.checked) {
-                isSingle = false;
-            } else {
-                isAllSelected = false;
-            }
-        }
-    }
-    if (isAllSelected) {
-        jQuery('#checkAllOrders').attr('checked', true);
-    } else {
-        jQuery('#checkAllOrders').attr('checked', false);
-    }
-    jQuery('#checkAllOrders').attr("checked", isAllSelected);
-    if (!isSingle && jQuery('#serviceName').val() != "") {
-        jQuery('#submitButton').removeAttr("disabled"); 
-    } else {
-        jQuery('#submitButton').attr('disabled', true);
-    }
-}
-
-// -->
-
-    function paginateOrderList(viewSize, viewIndex, hideFields) {
-        document.paginationForm.viewSize.value = viewSize;
-        document.paginationForm.viewIndex.value = viewIndex;
-        document.paginationForm.hideFields.value = hideFields;
-        document.paginationForm.submit();
-    }
-
-</script>
-
 <#if security.hasEntityPermission("ORDERMGR", "_VIEW", session)>
 <#if parameters.hideFields?has_content>
 <form name='lookupandhidefields${requestParameters.hideFields?default("Y")}' method="post" action="<@o...@ofbizUrl>">
diff --git a/applications/order/template/order/FindOrders.js.ftl b/applications/order/template/order/FindOrders.js.ftl
new file mode 100644
index 0000000..0b98270
--- /dev/null
+++ b/applications/order/template/order/FindOrders.js.ftl
@@ -0,0 +1,86 @@
+/***********************************************
+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.
+***********************************************/
+
+function lookupOrders(click) {
+    orderIdValue = document.lookuporder.orderId.value;
+    if (orderIdValue.length > 1) {
+        document.lookuporder.action = "<@o...@ofbizUrl>";
+        document.lookuporder.method = "get";
+    } else {
+        document.lookuporder.action = "<@o...@ofbizUrl>";
+    }
+
+    if (click) {
+        document.lookuporder.submit();
+    }
+    return true;
+}
+function toggleOrderId(master) {
+    var form = document.massOrderChangeForm;
+    var orders = form.elements.length;
+    for (var i = 0; i < orders; i++) {
+        var element = form.elements[i];
+        if ("orderIdList" == element.name) {
+            element.checked = master.checked;
+        }
+    }
+    toggleOrderIdList();
+}
+function setServiceName(selection) {
+    document.massOrderChangeForm.action = selection.value;
+}
+function runAction() {
+    var form = document.massOrderChangeForm;
+    form.submit();
+}
+
+function toggleOrderIdList() {
+    var form = document.massOrderChangeForm;
+    var orders = form.elements.length;
+    var isAllSelected = true;
+    var isSingle = true;
+    for (var i = 0; i < orders; i++) {
+        var element = form.elements[i];
+        if ("orderIdList" == element.name) {
+            if (element.checked) {
+                isSingle = false;
+            } else {
+                isAllSelected = false;
+            }
+        }
+    }
+    if (isAllSelected) {
+        jQuery('#checkAllOrders').attr('checked', true);
+    } else {
+        jQuery('#checkAllOrders').attr('checked', false);
+    }
+    jQuery('#checkAllOrders').attr("checked", isAllSelected);
+    if (!isSingle && jQuery('#serviceName').val() != "") {
+        jQuery('#submitButton').removeAttr("disabled");
+    } else {
+        jQuery('#submitButton').attr('disabled', true);
+    }
+}
+
+function paginateOrderList(viewSize, viewIndex, hideFields) {
+    document.paginationForm.viewSize.value = viewSize;
+    document.paginationForm.viewIndex.value = viewIndex;
+    document.paginationForm.hideFields.value = hideFields;
+    document.paginationForm.submit();
+}
\ No newline at end of file
diff --git a/applications/order/widget/ordermgr/OrderViewScreens.xml b/applications/order/widget/ordermgr/OrderViewScreens.xml
index 80c99db..7d0667c 100644
--- a/applications/order/widget/ordermgr/OrderViewScreens.xml
+++ b/applications/order/widget/ordermgr/OrderViewScreens.xml
@@ -266,7 +266,10 @@ under the License.
                     <decorator-section name="body">
                         <platform-specific><html><html-template location="component://common-theme/template/includes/SetMultipleSelectJs.ftl"/></html></platform-specific>
                         <platform-specific>
-                            <html><html-template location="component://order/template/order/FindOrders.ftl"/></html>
+                            <html>
+                                <script-template location="component://order/template/order/FindOrders.js.ftl"/>
+                                <html-template location="component://order/template/order/FindOrders.ftl"/>
+                            </html>
                         </platform-specific>
                     </decorator-section>
                 </decorator-screen>
diff --git a/framework/common/src/main/java/org/apache/ofbiz/common/CommonEvents.java b/framework/common/src/main/java/org/apache/ofbiz/common/CommonEvents.java
index 76aa710..c97fa36 100644
--- a/framework/common/src/main/java/org/apache/ofbiz/common/CommonEvents.java
+++ b/framework/common/src/main/java/org/apache/ofbiz/common/CommonEvents.java
@@ -53,6 +53,7 @@ import org.apache.ofbiz.entity.GenericValue;
 import org.apache.ofbiz.entity.util.EntityUtilProperties;
 import org.apache.ofbiz.webapp.control.JWTManager;
 import org.apache.ofbiz.webapp.control.LoginWorker;
+import org.apache.ofbiz.widget.model.ScriptTemplateUtil;
 import org.apache.ofbiz.widget.model.ThemeFactory;
 import org.apache.ofbiz.widget.renderer.VisualTheme;
 
@@ -176,6 +177,30 @@ public class CommonEvents {
         return "success";
     }
 
+    public static String jsResponseFromRequest(HttpServletRequest request, HttpServletResponse response) {
+
+        String fileName = request.getParameter("name");
+        String script = ScriptTemplateUtil.getScriptFromSession(request.getSession(), fileName);
+
+        // return the JS String
+        Writer out;
+        try {
+
+            // set the JS content type
+            response.setContentType("application/javascript");
+            // script.length is not reliable for unicode characters
+            response.setContentLength(script.getBytes("UTF8").length);
+
+            out = response.getWriter();
+            out.write(script);
+            out.flush();
+        } catch (IOException e) {
+            Debug.logError(e, MODULE);
+            return "error";
+        }
+        return "success";
+    }
+
     public static String jsonResponseFromRequestAttributes(HttpServletRequest request, HttpServletResponse response) {
         // pull out the service response from the request attribute
 
diff --git a/framework/common/webcommon/WEB-INF/common-controller.xml b/framework/common/webcommon/WEB-INF/common-controller.xml
index 46fa551..e6f9394 100644
--- a/framework/common/webcommon/WEB-INF/common-controller.xml
+++ b/framework/common/webcommon/WEB-INF/common-controller.xml
@@ -324,6 +324,17 @@ under the License.
         <response name="error" type="request" value="json"/>
     </request-map>
 
+    <request-map uri="getJs" method="get">
+        <security https="false" auth="false"/>
+        <response name="success" type="request" value="js"/>
+        <response name="error" type="request" value="js"/>
+    </request-map>
+    <request-map uri="js">
+        <security direct-request="false"/>
+        <event type="java" path="org.apache.ofbiz.common.CommonEvents" invoke="jsResponseFromRequest"/>
+        <response name="success" type="none"/>
+    </request-map>
+
     <!--========================== AJAX events =====================-->
 
     <!-- View Mappings -->
diff --git a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/ftl/ScriptTemplateListTransform.java b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/ftl/ScriptTemplateListTransform.java
new file mode 100644
index 0000000..c749c87
--- /dev/null
+++ b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/ftl/ScriptTemplateListTransform.java
@@ -0,0 +1,81 @@
+/*******************************************************************************
+ * 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 org.apache.ofbiz.webapp.ftl;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.ofbiz.widget.model.ScriptTemplateUtil;
+
+import freemarker.core.Environment;
+import freemarker.ext.beans.BeanModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateTransformModel;
+
+/**
+ * Render the script tags collected from the <script-template/>
+ */
+public class ScriptTemplateListTransform implements TemplateTransformModel {
+
+    public static final String MODULE = CsrfTokenAjaxTransform.class.getName();
+
+    @Override
+    public Writer getWriter(Writer out, @SuppressWarnings("rawtypes") Map args)
+            throws TemplateModelException, IOException {
+
+        return new Writer(out) {
+
+            @Override
+            public void close() throws IOException {
+                try {
+                    Environment env = Environment.getCurrentEnvironment();
+                    BeanModel req = (BeanModel) env.getVariable("request");
+                    if (req != null) {
+                        HttpServletRequest request = (HttpServletRequest) req.getWrappedObject();
+                        Set<String> scriptSrcSet = ScriptTemplateUtil.getScriptSrcLinksFromRequest(request);
+                        if (scriptSrcSet!=null) {
+                            String srcList = "";
+                            for (String scriptSrc : scriptSrcSet) {
+                                srcList += ("<script src=\"" + scriptSrc + "\" type=\"application/javascript\"></script>\n");
+                            }
+                            out.write(srcList);
+                        }
+                    }
+                    return;
+                } catch (Exception e) {
+                    throw new IOException(e.getMessage());
+                }
+            }
+
+            @Override
+            public void flush() throws IOException {
+                out.flush();
+            }
+
+            @Override
+            public void write(char cbuf[], int off, int len) {
+            }
+        };
+
+    }
+}
diff --git a/framework/webapp/src/main/resources/org/apache/ofbiz/webapp/freemarkerTransforms.properties b/framework/webapp/src/main/resources/org/apache/ofbiz/webapp/freemarkerTransforms.properties
index 65cf04e..b4f4d8b 100644
--- a/framework/webapp/src/main/resources/org/apache/ofbiz/webapp/freemarkerTransforms.properties
+++ b/framework/webapp/src/main/resources/org/apache/ofbiz/webapp/freemarkerTransforms.properties
@@ -31,3 +31,4 @@ renderWrappedText=org.apache.ofbiz.webapp.ftl.RenderWrappedTextTransform
 setContextField=org.apache.ofbiz.webapp.ftl.SetContextFieldTransform
 csrfTokenAjax=org.apache.ofbiz.webapp.ftl.CsrfTokenAjaxTransform
 csrfTokenPair=org.apache.ofbiz.webapp.ftl.CsrfTokenPairNonAjaxTransform
+scriptTemplateList=org.apache.ofbiz.webapp.ftl.ScriptTemplateListTransform
diff --git a/framework/widget/dtd/widget-screen.xsd b/framework/widget/dtd/widget-screen.xsd
index 7165f41..62ffc03 100644
--- a/framework/widget/dtd/widget-screen.xsd
+++ b/framework/widget/dtd/widget-screen.xsd
@@ -544,6 +544,14 @@ under the License.
             <xs:attribute type="xs:string" name="name" use="required" />
         </xs:complexType>
     </xs:element>
+    <xs:element name="script-template" substitutionGroup="HtmlWidgets">
+        <xs:complexType>
+            <xs:attributeGroup ref="attlist.script-template" />
+        </xs:complexType>
+    </xs:element>
+    <xs:attributeGroup name="attlist.script-template">
+        <xs:attribute type="xs:string" name="location" use="required" />
+    </xs:attributeGroup>
     <!-- ============== Swing Specific Elements =============== -->
     <xs:element name="swing">
         <xs:complexType />
diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/artifact/ArtifactInfoGatherer.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/artifact/ArtifactInfoGatherer.java
index 0ea3393..3c96f1e 100644
--- a/framework/widget/src/main/java/org/apache/ofbiz/widget/artifact/ArtifactInfoGatherer.java
+++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/artifact/ArtifactInfoGatherer.java
@@ -38,6 +38,7 @@ import org.apache.ofbiz.widget.model.HtmlWidget;
 import org.apache.ofbiz.widget.model.HtmlWidget.HtmlTemplate;
 import org.apache.ofbiz.widget.model.HtmlWidget.HtmlTemplateDecorator;
 import org.apache.ofbiz.widget.model.HtmlWidget.HtmlTemplateDecoratorSection;
+import org.apache.ofbiz.widget.model.HtmlWidget.ScriptTemplate;
 import org.apache.ofbiz.widget.model.IterateSectionWidget;
 import org.apache.ofbiz.widget.model.ModelAction;
 import org.apache.ofbiz.widget.model.ModelActionVisitor;
@@ -356,6 +357,10 @@ public final class ArtifactInfoGatherer implements ModelWidgetVisitor, ModelActi
     }
 
     @Override
+    public void visit(ScriptTemplate scriptTemplate) throws Exception {
+    }
+
+    @Override
     public void visit(Section section) throws Exception {
         for (ModelAction action : section.getActions()) {
             action.accept(this);
diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/HtmlWidget.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/HtmlWidget.java
index b39ec2d..b5afc68 100644
--- a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/HtmlWidget.java
+++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/HtmlWidget.java
@@ -19,6 +19,7 @@
 package org.apache.ofbiz.widget.model;
 
 import java.io.IOException;
+import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -120,6 +121,8 @@ public class HtmlWidget extends ModelScreenWidget {
                     subWidgets.add(new HtmlTemplate(modelScreen, childElement));
                 } else if ("html-template-decorator".equals(childElement.getNodeName())) {
                     subWidgets.add(new HtmlTemplateDecorator(modelScreen, childElement));
+                } else if ("script-template".equals(childElement.getNodeName())) {
+                    subWidgets.add(new ScriptTemplate(modelScreen, childElement));
                 } else {
                     throw new IllegalArgumentException("Tag not supported under the platform-specific -> html tag with name: "
                             + childElement.getNodeName());
@@ -175,6 +178,36 @@ public class HtmlWidget extends ModelScreenWidget {
         }
     }
 
+    public static void renderScriptTemplate(Appendable writer, FlexibleStringExpander locationExdr, Map<String, Object> context) {
+        String location = locationExdr.expandString(context);
+
+        if (UtilValidate.isEmpty(location)) {
+            throw new IllegalArgumentException("Template location is empty with search string location " + locationExdr.getOriginal());
+        }
+
+        if (location.endsWith(".ftl")) {
+            try {
+                boolean insertWidgetBoundaryComments = ModelWidget.widgetBoundaryCommentsEnabled(context);
+                if (insertWidgetBoundaryComments) {
+                    writer.append(HtmlWidgetRenderer.formatBoundaryJsComment("Begin", "Template", location));
+                }
+
+                Template template = FreeMarkerWorker.getTemplate(location, specialTemplateCache, specialConfig);
+                FreeMarkerWorker.renderTemplate(template, context, writer);
+
+                if (insertWidgetBoundaryComments) {
+                    writer.append(HtmlWidgetRenderer.formatBoundaryJsComment("End", "Template", location));
+                }
+            } catch (IllegalArgumentException | TemplateException | IOException e) {
+                String errMsg = "Error rendering included template at location [" + location + "]: " + e.toString();
+                Debug.logError(e, errMsg, MODULE);
+                writeError(writer, errMsg);
+            }
+        } else {
+            throw new IllegalArgumentException("Rendering not yet supported for the template at location: " + location);
+        }
+    }
+
     // TODO: We can make this more fancy, but for now this is very functional
     public static void writeError(Appendable writer, String message) {
         try {
@@ -288,6 +321,45 @@ public class HtmlWidget extends ModelScreenWidget {
         }
     }
 
+    public static class ScriptTemplate extends ModelScreenWidget {
+        protected FlexibleStringExpander locationExdr;
+
+        public ScriptTemplate(ModelScreen modelScreen, Element htmlTemplateElement) {
+            super(modelScreen, htmlTemplateElement);
+            this.locationExdr = FlexibleStringExpander.getInstance(htmlTemplateElement.getAttribute("location"));
+        }
+
+        public String getLocation(Map<String, Object> context) {
+            return locationExdr.expandString(context);
+        }
+
+        @Override
+        public void renderWidgetString(Appendable writer, Map<String, Object> context, ScreenStringRenderer screenStringRenderer) throws IOException {
+            StringWriter stringWriter = new StringWriter();
+            renderScriptTemplate(stringWriter, this.locationExdr, context);
+            String data = stringWriter.toString();
+            stringWriter.close();
+
+            String fileName = this.getLocation(context);
+            fileName = fileName.substring(fileName.lastIndexOf("/")+1);
+            // remove ".ftl"
+            fileName = fileName.substring(0, fileName.length()-4);
+            ScriptTemplateUtil.putScriptInSession(context, fileName, data);
+
+            String webappName = (String)context.get("webappName");
+            ScriptTemplateUtil.addScriptSrcToRequest(context, "/"+webappName+"/control/getJs?name="+fileName);
+        }
+
+        @Override
+        public void accept(ModelWidgetVisitor visitor) throws Exception {
+            visitor.visit(this);
+        }
+
+        public FlexibleStringExpander getLocationExdr() {
+            return locationExdr;
+        }
+    }
+
     @Override
     public void accept(ModelWidgetVisitor visitor) throws Exception {
         visitor.visit(this);
diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelWidgetVisitor.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelWidgetVisitor.java
index f081f98..a1f89bb 100644
--- a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelWidgetVisitor.java
+++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelWidgetVisitor.java
@@ -32,6 +32,8 @@ public interface ModelWidgetVisitor {
 
     void visit(HtmlWidget.HtmlTemplateDecoratorSection htmlTemplateDecoratorSection) throws Exception;
 
+    void visit(HtmlWidget.ScriptTemplate scriptTemplate) throws Exception;
+
     void visit(IterateSectionWidget iterateSectionWidget) throws Exception;
 
     void visit(ModelSingleForm modelForm) throws Exception;
diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ScriptTemplateUtil.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ScriptTemplateUtil.java
new file mode 100644
index 0000000..d6d16a2
--- /dev/null
+++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ScriptTemplateUtil.java
@@ -0,0 +1,92 @@
+/*******************************************************************************
+ * 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 org.apache.ofbiz.widget.model;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+import org.apache.ofbiz.base.util.UtilGenerics;
+import org.apache.ofbiz.webapp.ftl.ScriptTemplateListTransform;
+
+public class ScriptTemplateUtil {
+
+    private static String sessionKey = "ScriptTemplateMap";
+    private static String requestKey = "ScriptTemplateList";
+    private static int maxNumOfScriptInCache = 10;
+
+    /**
+     * add script src link for use by @see {@link ScriptTemplateListTransform}
+     * @param context
+     * @param filePath
+     */
+    public static void addScriptSrcToRequest(Map<String, Object> context, String filePath){
+        HttpServletRequest request = (HttpServletRequest)context.get("request");
+        Set<String> scriptTemplates = UtilGenerics.cast(request.getAttribute(requestKey));
+        if (scriptTemplates==null){
+            // use of LinkedHashSet to maintain insertion order
+            scriptTemplates = new LinkedHashSet<String>();
+            request.setAttribute(requestKey, scriptTemplates);
+        }
+        scriptTemplates.add(filePath);
+    }
+
+    /**
+     * get the script src links collected from the <script-template/> tags
+     * @param request
+     * @return
+     */
+    public static Set<String> getScriptSrcLinksFromRequest(HttpServletRequest request){
+        Set<String> scriptTemplates = UtilGenerics.cast(request.getAttribute(requestKey));
+        return scriptTemplates;
+    }
+
+    public static void putScriptInSession(Map<String, Object> context, String fileName, String fileContent){
+        HttpSession session = (HttpSession)context.get("session");
+        Map<String,String> scriptTemplateMap = UtilGenerics.cast(session.getAttribute(sessionKey));
+        if (scriptTemplateMap==null){
+            synchronized (session) {
+                scriptTemplateMap = UtilGenerics.cast(session.getAttribute(sessionKey));
+                if (scriptTemplateMap==null){
+                    // use of LinkedHashMap to limit size of the map
+                    scriptTemplateMap = new LinkedHashMap<String, String>() {
+                        private static final long serialVersionUID = 1L;
+                        protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
+                            return size() > maxNumOfScriptInCache;
+                        }
+                    };
+                    session.setAttribute(sessionKey, scriptTemplateMap);
+                }
+            }
+        }
+        scriptTemplateMap.put(fileName, fileContent);
+    }
+
+    public static String getScriptFromSession(HttpSession session, String fileName){
+        Map<String,String> scriptTemplateMap = UtilGenerics.cast(session.getAttribute(sessionKey));
+        if (scriptTemplateMap!=null){
+            return scriptTemplateMap.get(fileName);
+        }
+        return null;
+    }
+}
diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/XmlWidgetVisitor.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/XmlWidgetVisitor.java
index 193a45a..737ced1 100644
--- a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/XmlWidgetVisitor.java
+++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/XmlWidgetVisitor.java
@@ -24,6 +24,7 @@ import java.util.Map;
 import org.apache.ofbiz.widget.model.HtmlWidget.HtmlTemplate;
 import org.apache.ofbiz.widget.model.HtmlWidget.HtmlTemplateDecorator;
 import org.apache.ofbiz.widget.model.HtmlWidget.HtmlTemplateDecoratorSection;
+import org.apache.ofbiz.widget.model.HtmlWidget.ScriptTemplate;
 import org.apache.ofbiz.widget.model.ModelScreenWidget.Column;
 import org.apache.ofbiz.widget.model.ModelScreenWidget.ColumnContainer;
 import org.apache.ofbiz.widget.model.ModelScreenWidget.Container;
@@ -405,6 +406,14 @@ public class XmlWidgetVisitor extends XmlAbstractWidgetVisitor implements ModelW
     }
 
     @Override
+    public void visit(ScriptTemplate scriptTemplate) throws Exception {
+        writer.append("<script-template");
+        visitModelWidget(scriptTemplate);
+        visitAttribute("location", scriptTemplate.getLocationExdr());
+        writer.append("/>");
+    }
+
+    @Override
     public void visit(ModelScreen modelScreen) throws Exception {
         writer.append("<screen");
         visitModelWidget(modelScreen);
diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/html/HtmlWidgetRenderer.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/html/HtmlWidgetRenderer.java
index 62cb643..047037e 100644
--- a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/html/HtmlWidgetRenderer.java
+++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/html/HtmlWidgetRenderer.java
@@ -60,6 +60,10 @@ public class HtmlWidgetRenderer {
         return "<!-- " + boundaryType + " " + widgetType + " " + widgetName + " -->" + whiteSpace;
     }
 
+    public static String formatBoundaryJsComment(String boundaryType, String widgetType, String widgetName) {
+        return "// " + boundaryType + " " + widgetType + " " + widgetName + whiteSpace;
+    }
+
     /**
      * Renders the beginning boundary comment string.
      * @param writer The writer to write to
diff --git a/themes/flatgrey/template/Footer.ftl b/themes/flatgrey/template/Footer.ftl
index 517c1b0..3a65a83 100644
--- a/themes/flatgrey/template/Footer.ftl
+++ b/themes/flatgrey/template/Footer.ftl
@@ -42,4 +42,5 @@ under the License.
   </#list>
 </#if>
 </body>
+<@scriptTemplateList/>
 </html>
diff --git a/themes/rainbowstone/template/includes/Footer.ftl b/themes/rainbowstone/template/includes/Footer.ftl
index 164b5aa..057ad23 100644
--- a/themes/rainbowstone/template/includes/Footer.ftl
+++ b/themes/rainbowstone/template/includes/Footer.ftl
@@ -34,4 +34,5 @@ under the License.
   </#list>
 </#if>
 </body>
+<@scriptTemplateList/>
 </html>
diff --git a/themes/tomahawk/template/Footer.ftl b/themes/tomahawk/template/Footer.ftl
index cc26dd7..baf94a8 100644
--- a/themes/tomahawk/template/Footer.ftl
+++ b/themes/tomahawk/template/Footer.ftl
@@ -42,4 +42,5 @@ under the License.
 
 </div>
 </body>
+<@scriptTemplateList/>
 </html>