1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
79
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
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
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();
109 final TobagoContext tobagoContext = CDI.current().select(TobagoContext.class).get();
110
111 if (tobagoContext.getFocusId() == null && !StringUtils.isBlank(component.getFocusId())) {
112 tobagoContext.setFocusId(component.getFocusId());
113 }
114 final TobagoResponseWriter writer = getResponseWriter(facesContext);
115
116
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
188 for (final UIComponent metas : headResources.getMetas()) {
189 metas.encodeAll(facesContext);
190 }
191
192
193 writer.startElement(HtmlElements.TITLE);
194 writer.writeText(title != null ? title : "");
195 writer.endElement(HtmlElements.TITLE);
196
197
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
210 for (final UIComponent styles : headResources.getStyles()) {
211 styles.encodeAll(facesContext);
212 }
213
214
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
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
269 writer.writeAttribute(HtmlAttributes.ACCEPT_CHARSET, AbstractUIPage.FORM_ACCEPT_CHARSET.name(), false);
270
271
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
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
310
311
312
313
314
315
316
317
318
319
320
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
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
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
406
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 }