Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion impl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,9 @@
</includes>
<outputFilename>#{path}/faces.js</outputFilename>
<skipMerge>true</skipMerge>
<closureLanguageOut>NO_TRANSPILE</closureLanguageOut>
<!-- Transpile down to ES5 so the minified faces.js parses on HtmlUnit's bundled Rhino as used in 4.x TCK.
Rhino's effective compatibility is ES5-ish. This config is no longer used in 5.0 branch. -->
<closureLanguageOut>ECMASCRIPT5</closureLanguageOut>
<closureEmitUseStrict>false</closureEmitUseStrict>
</configuration>
</execution>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
import static jakarta.faces.application.ProjectStage.Development;
import static jakarta.faces.application.Resource.COMPONENT_RESOURCE_KEY;
import static jakarta.faces.application.StateManager.IS_BUILDING_INITIAL_STATE;
import static jakarta.faces.application.StateManager.STATE_SAVING_METHOD_SERVER;
import static jakarta.faces.application.StateManager.STATE_SAVING_METHOD_CLIENT;
import static jakarta.faces.application.ViewHandler.CHARACTER_ENCODING_KEY;
import static jakarta.faces.application.ViewHandler.DEFAULT_FACELETS_SUFFIX;
import static jakarta.faces.application.ViewVisitOption.RETURN_AS_MINIMAL_IMPLICIT_OUTCOME;
Expand Down Expand Up @@ -91,6 +91,7 @@
import jakarta.faces.component.Doctype;
import jakarta.faces.component.EditableValueHolder;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.UIForm;
import jakarta.faces.component.UIPanel;
import jakarta.faces.component.UIViewRoot;
import jakarta.faces.component.html.HtmlDoctype;
Expand Down Expand Up @@ -410,7 +411,7 @@ public void renderView(FacesContext ctx, UIViewRoot viewToRender) throws IOExcep
*
* Note if you flag a view as transient then we won't acquire the session as you are stating it does not need one.
*/
if (isServerStateSaving() && !viewToRender.isTransient()) {
if (!viewToRender.isTransient() && extContext.getSession(false) == null && isServerStateSaving() && hasForm(ctx, viewToRender)) {
getSession(ctx);
}

Expand Down Expand Up @@ -1834,11 +1835,7 @@ private void reapplyDynamicRemove(FacesContext context, ComponentStruct struct)
* @return true if we are, false otherwise.
*/
private boolean isServerStateSaving() {
if (STATE_SAVING_METHOD_SERVER.equals(webConfig.getOptionValue(StateSavingMethod))) {
return true;
}

return false;
return !STATE_SAVING_METHOD_CLIENT.equalsIgnoreCase(webConfig.getOptionValue(StateSavingMethod));
}

/**
Expand All @@ -1856,6 +1853,25 @@ private HttpSession getSession(FacesContext context) {
return null;
}

/**
* @return true if the view contains at least one {@link UIForm}, in which case state will be written and a session
* is needed under server-side state saving.
*/
private static boolean hasForm(FacesContext context, UIViewRoot viewRoot) {
if (viewRoot == null || viewRoot.getChildCount() == 0) {
return false;
}
boolean[] found = { false };
viewRoot.visitTree(VisitContext.createVisitContext(context), (visitContext, target) -> {
if (target instanceof UIForm) {
found[0] = true;
return VisitResult.COMPLETE;
}
return VisitResult.ACCEPT;
});
return found[0];
}

/**
* Gets and if needed initializes the faceletFactory
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ public void processPartial(PhaseId phaseId) {
if (isRenderAll()) {
renderAll(ctx, viewRoot);
renderState(ctx);
renderEvalScripts(ctx);
doFlashPostPhaseActions(ctx);
writer.endDocument();
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ public void encodeEnd(FacesContext context, UIComponent component) throws IOExce
writer.endElement("span");
} else {
writer.endElement("a");

String target = (String) component.getAttributes().get("target");
if (target != null) {
target = target.trim();
} else {
target = "";
}
Collection<ClientBehaviorContext.Parameter> params = getBehaviorParameters(component);
RenderKitUtils.renderOnclickEventListener(context, component, params, target, true);
}
}

Expand Down Expand Up @@ -161,16 +170,6 @@ protected void renderAsActive(FacesContext context, UIComponent command) throws
// render the current value as link text.
writeValue(command, writer);
writer.flush();

String target = (String) command.getAttributes().get("target");
if (target != null) {
target = target.trim();
} else {
target = "";
}

Collection<ClientBehaviorContext.Parameter> params = getBehaviorParameters(command);
RenderKitUtils.renderOnclickEventListener(context, command, params, target, true);
}

// --------------------------------------------------------- Private Methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import com.sun.faces.util.Util;

import jakarta.faces.component.NamingContainer;
import jakarta.faces.component.UICommand;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.UIInput;
import jakarta.faces.component.UIParameter;
Expand Down Expand Up @@ -579,13 +580,16 @@ protected boolean shouldWriteIdAttribute(UIComponent component) {
// By default we only write the id attribute if:
//
// - We have a non-auto-generated id, or...
// - We have client behaviors.
// - We have client behaviors, or...
// - The component is a UICommand (its CSP-style click listener is attached
// via mojarra.ael(clientId, ...) which needs the id on the rendered element).
//
// We assume that if client behaviors are present, they
// may need access to the id (AjaxBehavior certainly does).

String id;
return null != (id = component.getId()) && (!id.startsWith(UIViewRoot.UNIQUE_ID_PREFIX)
|| component instanceof UICommand
|| component instanceof ClientBehaviorHolder && !((ClientBehaviorHolder) component).getClientBehaviors().isEmpty());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,6 @@ if ( !( (window.faces && window.faces.specversion && window.faces.specversion >=
return inputElementName in form ? form[inputElementName] : getElementByName(form,inputElementName);
}

/**
* append a new pair of parameter=value to a query string
* @ignore
*/

/**
* return true if one of the dom elements contains
Expand Down Expand Up @@ -355,12 +351,19 @@ if ( !( (window.faces && window.faces.specversion && window.faces.specversion >=
};

/**
* Remove all the portion of code matching the script pattern from the passed string
* Remove all the portion of code matching the script pattern from the passed string,
* preserving scripts whose type is set to something other than text/javascript.
* @param html a String containing a portion of html
* @ignore
*/
const removeScripts = function removeScripts(html) {
return html.replace(/<script[^>]*type="text\/javascript"[^>]*>([\S\s]*?)<\/script>/igm, EMPTY);
return html.replace(/<script[^>]*>([\S\s]*?)<\/script>/igm, (match) => {
const type = match.match(TAG_ATTRIBUTE_TYPE_REGEX);
if (!!type && type[1] !== "text/javascript") {
return match; // keep non-text/javascript scripts
}
return EMPTY;
});
};

/**
Expand Down Expand Up @@ -3222,17 +3225,9 @@ mojarra.l = function l(l) {
if (document.readyState === "complete") {
setTimeout(l);
}
else if (window.addEventListener) {
window.addEventListener("load", l, false);
}
else if (typeof window.onload === "function") {
const oldListener = window.onload;
window.onload = function() { oldListener(); l(); };
}
else {
window.onload = l;
window.addEventListener("load", l);
}

};

/**
Expand Down
52 changes: 52 additions & 0 deletions impl/src/test/ts/spec/faces.ajax.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,58 @@ describe("faces.ajax.response", () => {
expect(created!.value).toBe("sharedState");
otherForm.remove();
});

test("update jakarta.faces.Resource injects new <link> stylesheet into head", () => {
const beforeLinks = document.head.querySelectorAll("link").length;
ajax().request(button, null, { render: "testForm" });
const href = "/test/jakarta.faces.resource/issue4345.css.xhtml?firstParam=1&amp;secondParam=2";
const xml = successResponse(
`<update id="jakarta.faces.Resource"><![CDATA[<link type="text/css" rel="stylesheet" href="${href}" />]]></update>`
);
lastXHR().respond(200, "", xml);

const links = document.head.querySelectorAll("link");
expect(links.length).toBe(beforeLinks + 1);
const injected = links[links.length - 1];
expect(injected.getAttribute("rel")).toBe("stylesheet");
// href is unescaped before being assigned to the DOM property
expect(injected.href).toContain("issue4345.css.xhtml?firstParam=1&secondParam=2");
injected.remove();
});

test("update jakarta.faces.Resource skips <link> already present in head", () => {
const href = "/test/jakarta.faces.resource/already-loaded.css.xhtml";
const existing = document.createElement("link");
existing.setAttribute("type", "text/css");
existing.setAttribute("rel", "stylesheet");
existing.setAttribute("href", href);
document.head.appendChild(existing);

const beforeLinks = document.head.querySelectorAll("link").length;
ajax().request(button, null, { render: "testForm" });
const xml = successResponse(
`<update id="jakarta.faces.Resource"><![CDATA[<link type="text/css" rel="stylesheet" href="${href}" />]]></update>`
);
lastXHR().respond(200, "", xml);

expect(document.head.querySelectorAll("link").length).toBe(beforeLinks);
existing.remove();
});

test("update jakarta.faces.Resource handles multiple <link> tags without throwing", () => {
const beforeLinks = document.head.querySelectorAll("link").length;
ajax().request(button, null, { render: "testForm" });
const xml = successResponse(
'<update id="jakarta.faces.Resource"><![CDATA[' +
'<link type="text/css" rel="stylesheet" href="/test/a.css" />' +
'<link type="text/css" rel="stylesheet" href="/test/b.css" />' +
']]></update>'
);
expect(() => lastXHR().respond(200, "", xml)).not.toThrow();

expect(document.head.querySelectorAll("link").length).toBe(beforeLinks + 2);
document.head.querySelectorAll('link[href^="/test/"]').forEach(el => el.remove());
});
});

// ---- HTTP error codes ----
Expand Down
2 changes: 1 addition & 1 deletion impl/src/test/ts/spec/mojarra.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ describe("mojarra.l", () => {

(moj().l as Function)(callback);

expect(spy).toHaveBeenCalledWith("load", callback, false);
expect(spy).toHaveBeenCalledWith("load", callback);

spy.mockRestore();
if (originalReadyState) {
Expand Down
Loading