View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.tobago.internal.renderkit.renderer;
21  
22  import org.apache.myfaces.tobago.component.Attributes;
23  import org.apache.myfaces.tobago.component.RendererTypes;
24  import org.apache.myfaces.tobago.component.Tags;
25  import org.apache.myfaces.tobago.config.TobagoConfig;
26  import org.apache.myfaces.tobago.context.Markup;
27  import org.apache.myfaces.tobago.context.Theme;
28  import org.apache.myfaces.tobago.context.ThemeScript;
29  import org.apache.myfaces.tobago.context.ThemeStyle;
30  import org.apache.myfaces.tobago.context.TobagoContext;
31  import org.apache.myfaces.tobago.internal.component.AbstractUIMeta;
32  import org.apache.myfaces.tobago.internal.component.AbstractUIMetaLink;
33  import org.apache.myfaces.tobago.internal.component.AbstractUIPage;
34  import org.apache.myfaces.tobago.internal.component.AbstractUIScript;
35  import org.apache.myfaces.tobago.internal.component.AbstractUIStyle;
36  import org.apache.myfaces.tobago.internal.util.AccessKeyLogger;
37  import org.apache.myfaces.tobago.internal.util.CookieUtils;
38  import org.apache.myfaces.tobago.internal.util.HtmlRendererUtils;
39  import org.apache.myfaces.tobago.internal.util.ResponseUtils;
40  import org.apache.myfaces.tobago.internal.util.StringUtils;
41  import org.apache.myfaces.tobago.portlet.PortletUtils;
42  import org.apache.myfaces.tobago.renderkit.RendererBase;
43  import org.apache.myfaces.tobago.renderkit.css.BootstrapClass;
44  import org.apache.myfaces.tobago.renderkit.css.TobagoClass;
45  import org.apache.myfaces.tobago.renderkit.html.CustomAttributes;
46  import org.apache.myfaces.tobago.renderkit.html.DataAttributes;
47  import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
48  import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
49  import org.apache.myfaces.tobago.renderkit.html.HtmlInputTypes;
50  import org.apache.myfaces.tobago.util.ComponentUtils;
51  import org.apache.myfaces.tobago.util.ResourceUtils;
52  import org.apache.myfaces.tobago.webapp.Secret;
53  import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  
57  import javax.enterprise.inject.spi.CDI;
58  import javax.faces.application.Application;
59  import javax.faces.application.ProjectStage;
60  import javax.faces.application.ViewHandler;
61  import javax.faces.component.UIComponent;
62  import javax.faces.component.UIOutput;
63  import javax.faces.component.UIViewRoot;
64  import javax.faces.context.ExternalContext;
65  import javax.faces.context.FacesContext;
66  import javax.portlet.MimeResponse;
67  import javax.portlet.ResourceURL;
68  import javax.servlet.http.HttpServletRequest;
69  import javax.servlet.http.HttpServletResponse;
70  import java.io.IOException;
71  import java.lang.invoke.MethodHandles;
72  import java.util.ArrayList;
73  import java.util.Collection;
74  import java.util.List;
75  import java.util.Locale;
76  import java.util.Map;
77  
78  // using jsf.js from a specific MyFaces version instead, to avoid old bugs
79  //@ResourceDependency(name="jsf.js", library="javax.faces", target="head")
80  public class PageRenderer<T extends AbstractUIPage> extends RendererBase<T> {
81  
82    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
83  
84    private static final String LAST_FOCUS_ID = "lastFocusId";
85    private static final String HEAD_TARGET = "head";
86    private static final String BODY_TARGET = "body";
87  
88    @Override
89    public void decodeInternal(final FacesContext facesContext, final T component) {
90  
91      final String clientId = component.getClientId(facesContext);
92      final ExternalContext externalContext = facesContext.getExternalContext();
93  
94      // last focus
95      final String lastFocusId =
96          externalContext.getRequestParameterMap().get(clientId + ComponentUtils.SUB_SEPARATOR + LAST_FOCUS_ID);
97      if (lastFocusId != null) {
98        TobagoContext.getInstance(facesContext).setFocusId(lastFocusId);
99      }
100   }
101 
102 //  @Inject // fixme
103   private ProjectStage projectStage;
104 
105   @Override
106   public void encodeBeginInternal(final FacesContext facesContext, final T component) throws IOException {
107 
108     final TobagoConfig tobagoConfig = CDI.current().select(TobagoConfig.class).get(); // todo: may inject
109     final TobagoContext tobagoContext = CDI.current().select(TobagoContext.class).get(); // todo: may inject
110 
111     if (tobagoContext.getFocusId() == null && !StringUtils.isBlank(component.getFocusId())) {
112       tobagoContext.setFocusId(component.getFocusId());
113     }
114     final TobagoResponseWriter writer = getResponseWriter(facesContext);
115 
116     // reset responseWriter and render page
117     facesContext.setResponseWriter(writer);
118 
119     if (tobagoConfig.isPreventFrameAttacks()) {
120       ResponseUtils.ensureXFrameOptionsHeader(facesContext);
121     }
122 
123     ResponseUtils.ensureNoCacheHeader(facesContext);
124 
125     ResponseUtils.ensureContentSecurityPolicyHeader(facesContext, tobagoConfig.getContentSecurityPolicy());
126 
127     if (LOG.isDebugEnabled()) {
128       for (final Object o : component.getAttributes().entrySet()) {
129         final Map.Entry entry = (Map.Entry) o;
130         LOG.debug("*** '" + entry.getKey() + "' -> '" + entry.getValue() + "'");
131       }
132     }
133 
134     final ExternalContext externalContext = facesContext.getExternalContext();
135     final String contextPath = externalContext.getRequestContextPath();
136     final Object request = externalContext.getRequest();
137     final Object response = externalContext.getResponse();
138     final Application application = facesContext.getApplication();
139     final ViewHandler viewHandler = application.getViewHandler();
140     final UIViewRoot viewRoot = facesContext.getViewRoot();
141     final String viewId = viewRoot.getViewId();
142     final String formAction = externalContext.encodeActionURL(viewHandler.getActionURL(facesContext, viewId));
143     final String partialAction;
144     final boolean portlet = PortletUtils.isPortletApiAvailable() && response instanceof MimeResponse;
145     if (portlet) {
146       final MimeResponse mimeResponse = (MimeResponse) response;
147       final ResourceURL resourceURL = mimeResponse.createResourceURL();
148       partialAction = externalContext.encodeResourceURL(resourceURL.toString());
149     } else {
150       partialAction = null;
151     }
152 
153     final String contentType = writer.getContentTypeWithCharSet();
154     ResponseUtils.ensureContentTypeHeader(facesContext, contentType);
155     if (tobagoConfig.isSetNosniffHeader()) {
156       ResponseUtils.ensureNosniffHeader(facesContext);
157     }
158 
159     final Theme theme = tobagoContext.getTheme();
160     if (response instanceof HttpServletResponse && request instanceof HttpServletRequest) {
161       CookieUtils.setThemeNameToCookie((HttpServletRequest) request, (HttpServletResponse) response, theme.getName());
162     }
163 
164     final String clientId = component.getClientId(facesContext);
165     final boolean productionMode = projectStage == ProjectStage.Production;
166     final Markup markup = component.getMarkup();
167     final TobagoClass spread = markup != null && markup.contains(Markup.SPREAD) ? TobagoClass.SPREAD : null;
168     final String title = component.getLabel();
169 
170     final Locale locale = viewRoot.getLocale();
171     if (!portlet) {
172       writer.startElement(HtmlElements.HTML);
173       if (locale != null) {
174         final String language = locale.getLanguage();
175         if (language != null) {
176           writer.writeAttribute(HtmlAttributes.LANG, language, false);
177         }
178       }
179     }
180     writer.writeClassAttribute(spread);
181 
182     writer.startElement(HtmlElements.HEAD);
183 
184     final HeadResources headResources = new HeadResources(
185         facesContext, viewRoot.getComponentResources(facesContext, HEAD_TARGET), writer.getCharacterEncoding());
186 
187     // meta tags
188     for (final UIComponent metas : headResources.getMetas()) {
189       metas.encodeAll(facesContext);
190     }
191 
192     // title
193     writer.startElement(HtmlElements.TITLE);
194     writer.writeText(title != null ? title : "");
195     writer.endElement(HtmlElements.TITLE);
196 
197     // style files from theme
198     AbstractUIStyle style = null;
199     for (final ThemeStyle themeStyle : theme.getStyleResources(productionMode)) {
200       if (style == null) {
201         style = (AbstractUIStyle) facesContext.getApplication()
202            .createComponent(facesContext, Tags.style.componentType(), RendererTypes.Style.name());
203         style.setTransient(true);
204       }
205       style.setFile(contextPath + themeStyle.getName());
206       style.encodeAll(facesContext);
207     }
208 
209     // style files individual files
210     for (final UIComponent styles : headResources.getStyles()) {
211       styles.encodeAll(facesContext);
212     }
213 
214     // script files from theme
215     for (final ThemeScript themeScript : theme.getScriptResources(productionMode)) {
216       final AbstractUIScript script = (AbstractUIScript) facesContext.getApplication()
217           .createComponent(facesContext, Tags.script.componentType(), RendererTypes.Script.name());
218       script.setTransient(true);
219       script.setFile(contextPath + themeScript.getName());
220       script.setType(themeScript.getType());
221       script.encodeAll(facesContext);
222     }
223 
224     // script files individual files
225     for (final UIComponent scripts : headResources.getScripts()) {
226       scripts.encodeAll(facesContext);
227     }
228 
229     for (final UIComponent misc : headResources.getMisc()) {
230       misc.encodeAll(facesContext);
231     }
232 
233     writer.endElement(HtmlElements.HEAD);
234 
235     if (!portlet) {
236       writer.startElement(HtmlElements.BODY);
237       writer.writeClassAttribute(spread);
238     }
239 
240     writer.startElement(HtmlElements.TOBAGO_PAGE);
241 
242     writer.writeAttribute(CustomAttributes.LOCALE, locale.toString(), false);
243     writer.writeClassAttribute(
244         BootstrapClass.CONTAINER_FLUID,
245         TobagoClass.PAGE.createMarkup(portlet ? Markup.PORTLET.add(component.getMarkup()) : component.getMarkup()),
246         spread,
247         component.getCustomClass());
248     writer.writeIdAttribute(clientId);
249     HtmlRendererUtils.writeDataAttributes(facesContext, writer, component);
250 
251     encodeBehavior(writer, facesContext, component);
252 
253     writer.startElement(HtmlElements.FORM);
254     writer.writeClassAttribute(spread);
255     writer.writeAttribute(HtmlAttributes.ACTION, formAction, true);
256     if (partialAction != null) {
257       writer.writeAttribute(DataAttributes.PARTIAL_ACTION, partialAction, true);
258     }
259     if (LOG.isDebugEnabled()) {
260       LOG.debug("partial action = " + partialAction);
261     }
262     writer.writeIdAttribute(component.getFormId(facesContext));
263     writer.writeAttribute(HtmlAttributes.METHOD, getMethod(component), false);
264     final String enctype = tobagoContext.getEnctype();
265     if (enctype != null) {
266       writer.writeAttribute(HtmlAttributes.ENCTYPE, enctype, false);
267     }
268     // TODO: enable configuration of  'accept-charset'
269     writer.writeAttribute(HtmlAttributes.ACCEPT_CHARSET, AbstractUIPage.FORM_ACCEPT_CHARSET.name(), false);
270     // TODO evaluate 'accept' attribute usage
271     //writer.writeAttribute(HtmlAttributes.ACCEPT, );
272     writer.writeAttribute(DataAttributes.CONTEXT_PATH, contextPath, true);
273 
274     writer.startElement(HtmlElements.INPUT);
275     writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN);
276     writer.writeNameAttribute("javax.faces.source");
277     writer.writeIdAttribute("javax.faces.source");
278     writer.writeAttribute(HtmlAttributes.DISABLED, true);
279     writer.endElement(HtmlElements.INPUT);
280 
281     final String lastFocusId = clientId + ComponentUtils.SUB_SEPARATOR + "lastFocusId";
282     writer.startElement(HtmlElements.TOBAGO_FOCUS);
283     writer.writeIdAttribute(lastFocusId);
284     writer.startElement(HtmlElements.INPUT);
285     writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN);
286     writer.writeNameAttribute(lastFocusId);
287     writer.writeIdAttribute(lastFocusId + ComponentUtils.SUB_SEPARATOR + "field");
288     writer.writeAttribute(HtmlAttributes.VALUE, tobagoContext.getFocusId(), true);
289     writer.endElement(HtmlElements.INPUT);
290     writer.endElement(HtmlElements.TOBAGO_FOCUS);
291 
292     if (tobagoConfig.isCheckSessionSecret()) {
293       writer.startElement(HtmlElements.INPUT);
294       writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN);
295       writer.writeAttribute(HtmlAttributes.NAME, Secret.KEY, false);
296       writer.writeAttribute(HtmlAttributes.ID, Secret.KEY, false);
297 //      final Object session = facesContext.getExternalContext().getSession(true);
298       final Secret secret = CDI.current().select(Secret.class).get();
299       secret.encode(writer);
300       writer.endElement(HtmlElements.INPUT);
301     }
302 
303     if (component.getFacet("backButtonDetector") != null) {
304       final UIComponent hidden = component.getFacet("backButtonDetector");
305       hidden.encodeAll(facesContext);
306     }
307   }
308 
309 // TODO: this is needed for the "BACK-BUTTON-PROBLEM"
310 // but may no longer needed
311 /*
312     if (ViewHandlerImpl.USE_VIEW_MAP) {
313       writer.startElement(HtmlElements.INPUT, null);
314       writer.writeAttribute(HtmlAttributes.type, "hidden", null);
315       writer.writeNameAttribute(ViewHandlerImpl.PAGE_ID);
316       writer.writeIdAttribute(ViewHandlerImpl.PAGE_ID);
317       Object value = facesContext.getViewRoot().getAttributes().get(
318           ViewHandlerImpl.PAGE_ID);
319       writer.writeAttribute(HtmlAttributes.value, (value != null ? value : ""), null);
320       writer.endElement(HtmlElements.INPUT);
321     }
322 */
323 
324   @Override
325   public void encodeEndInternal(final FacesContext facesContext, final T component) throws IOException {
326 
327     final UIViewRoot viewRoot = facesContext.getViewRoot();
328     final TobagoResponseWriter writer = getResponseWriter(facesContext);
329     final String clientId = component.getClientId(facesContext);
330     final Application application = facesContext.getApplication();
331     final ViewHandler viewHandler = application.getViewHandler();
332     final Object response = facesContext.getExternalContext().getResponse();
333     final boolean portlet = PortletUtils.isPortletApiAvailable() && response instanceof MimeResponse;
334     final boolean ajax = facesContext.getPartialViewContext().isAjaxRequest();
335 
336     // placeholder for menus
337     writer.startElement(HtmlElements.DIV);
338     writer.writeClassAttribute(TobagoClass.PAGE__MENU_STORE);
339     writer.endElement(HtmlElements.DIV);
340 
341     writer.startElement(HtmlElements.SPAN);
342     writer.writeIdAttribute(clientId + ComponentUtils.SUB_SEPARATOR + "jsf-state-container");
343     writer.flush();
344     if (!ajax) {
345       viewHandler.writeState(facesContext);
346     }
347     writer.endElement(HtmlElements.SPAN);
348 
349     writer.endElement(HtmlElements.FORM);
350 
351     writer.startElement(HtmlElements.NOSCRIPT);
352     writer.startElement(HtmlElements.DIV);
353     writer.writeClassAttribute(TobagoClass.PAGE__NOSCRIPT);
354     writer.writeText(ResourceUtils.getString(facesContext, "page.noscript"));
355     writer.endElement(HtmlElements.DIV);
356     writer.endElement(HtmlElements.NOSCRIPT);
357     writer.endElement(HtmlElements.TOBAGO_PAGE);
358 
359     final List<UIComponent> bodyResources = viewRoot.getComponentResources(facesContext, BODY_TARGET);
360     for (final UIComponent bodyResource : bodyResources) {
361       bodyResource.encodeAll(facesContext);
362     }
363 
364     if (!portlet) {
365       writer.endElement(HtmlElements.BODY);
366       writer.endElement(HtmlElements.HTML);
367     }
368 
369     AccessKeyLogger.logStatus(facesContext);
370   }
371 
372   private String getMethod(final AbstractUIPage page) {
373     return ComponentUtils.getStringAttribute(page, Attributes.method, "post");
374   }
375 
376   @Override
377   public boolean getRendersChildren() {
378     return true;
379   }
380 
381   /**
382    * This class helps to order the head resources.
383    */
384   private static class HeadResources {
385 
386     private List<UIComponent> metas = new ArrayList<>();
387     private List<UIComponent> styles = new ArrayList<>();
388     private List<UIComponent> scripts = new ArrayList<>();
389     private List<UIComponent> misc = new ArrayList<>();
390 
391     HeadResources(
392         final FacesContext facesContext, final Collection<? extends UIComponent> collection, final String charset) {
393       for (final UIComponent uiComponent : collection) {
394         if (uiComponent instanceof AbstractUIMeta || uiComponent instanceof AbstractUIMetaLink) {
395           metas.add(uiComponent);
396         } else if (uiComponent instanceof AbstractUIStyle) {
397           styles.add(uiComponent);
398         } else if (uiComponent instanceof AbstractUIScript) {
399           scripts.add(uiComponent);
400         } else {
401           if (uiComponent instanceof UIOutput) {
402             final Map<String, Object> attributes = uiComponent.getAttributes();
403             if ("javax.faces".equals(attributes.get("library"))
404                 && "jsf.js".equals(attributes.get("name"))) {
405               // workaround for WebSphere
406               // We don't need jsf.js from the JSF impl, because Tobago comes with its own jsf.js
407               if (LOG.isDebugEnabled()) {
408                 LOG.debug("Skip rendering resource jsf.js");
409               }
410               continue;
411             }
412           }
413           misc.add(uiComponent);
414         }
415       }
416 
417       if (!containsNameViewport(metas)) {
418         final AbstractUIMeta viewportMeta = (AbstractUIMeta) facesContext.getApplication()
419             .createComponent(facesContext, Tags.meta.componentType(), RendererTypes.Meta.name());
420         viewportMeta.setName("viewport");
421         viewportMeta.setContent("width=device-width, initial-scale=1.0");
422         viewportMeta.setTransient(true);
423         metas.add(0, viewportMeta);
424       }
425 
426       if (!containsCharset(metas)) {
427         final AbstractUIMeta charsetMeta = (AbstractUIMeta) facesContext.getApplication()
428             .createComponent(facesContext, Tags.meta.componentType(), RendererTypes.Meta.name());
429         charsetMeta.setCharset(charset);
430         charsetMeta.setTransient(true);
431         metas.add(0, charsetMeta);
432       }
433     }
434 
435     public List<UIComponent> getMetas() {
436       return metas;
437     }
438 
439     public List<UIComponent> getStyles() {
440       return styles;
441     }
442 
443     public List<UIComponent> getScripts() {
444       return scripts;
445     }
446 
447     public List<UIComponent> getMisc() {
448       return misc;
449     }
450 
451     private boolean containsCharset(final List<UIComponent> headComponents) {
452       for (final UIComponent headComponent : headComponents) {
453         if (headComponent instanceof AbstractUIMeta
454             && ((AbstractUIMeta) headComponent).getCharset() != null) {
455           return true;
456         }
457       }
458       return false;
459     }
460 
461     private boolean containsNameViewport(final List<UIComponent> headComponents) {
462       for (final UIComponent headComponent : headComponents) {
463         if (headComponent instanceof AbstractUIMeta
464             && "viewport".equals(((AbstractUIMeta) headComponent).getName())) {
465           return true;
466         }
467       }
468       return false;
469     }
470 
471   }
472 }