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,798 changes: 2,363 additions & 2,435 deletions api/src/client/package-lock.json

Large diffs are not rendered by default.

36 changes: 20 additions & 16 deletions api/src/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,38 @@
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@microsoft/api-extractor": "^7.58.8",
"@microsoft/api-extractor": "^7.59.0",
"@types/chai": "^5.2.3",
"@types/fs-extra": "^11.0.4",
"@types/mocha": "^10.0.10",
"@types/node": "^25.9.3",
"@types/sinon": "^21.0.1",
"@types/node": "^26.3.0",
"@types/sinon": "^22.0.0",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"chai": "^6.2.2",
"compression-webpack-plugin": "^12.0.0",
"copy-webpack-plugin": "^14.0.0",
"cross-env": "^10.1.0",
"fs-extra": "^11.3.5",
"fs-extra": "^11.4.0",
"global-jsdom": "^29.0.0",
"html-webpack-plugin": "^5.6.7",
"html-webpack-plugin": "^5.6.8",
"jsdom": "^29.1.1",
"jsf.js_next_gen": "4.1.0-beta.20",
"mocha": "^11.7.6",
"jsf.js_next_gen": "4.1.1",
"mocha": "^11.8.0",
"nise": "^6.1.5",
"npm-check-updates": "^22.2.3",
"npm-check-updates": "^23.1.0",
"nyc": "^18.0.0",
"replace-in-file": "^8.4.0",
"replace-in-file": "^9.0.0",
"rxjs": "^7.8.2",
"sinon": "^22.0.0",
"sinon": "^22.1.0",
"terser-webpack-plugin": "^5.6.1",
"ts-loader": "^9.6.0",
"ts-loader": "^9.6.2",
"ts-node": "^10.9.2",
"tsx": "^4.22.4",
"typedoc": "^0.28.19",
"tsx": "^4.23.12",
"typedoc": "^0.28.20",
"typescript": "^6.0.3",
"webpack": "^5.107.2",
"webpack-cli": "^7.0.3",
"webpack-dev-server": "^5.2.5"
"webpack": "^5.109.2",
"webpack-cli": "^7.2.2",
"webpack-dev-server": "^6.0.0"
},
"ts-node": {
"compilerOptions": {
Expand All @@ -56,5 +56,9 @@
"diff": ">=8.0.3",
"serialize-javascript": ">=7.0.5",
"uuid": ">=14.0.0"
},
"allowScripts": {
"esbuild@0.28.1": true,
"fsevents@2.3.3": true
}
}
1 change: 1 addition & 0 deletions api/src/client/typescript/faces/impl/core/Const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export const ATTR_URL = "url";
export const ATTR_NAME = "name";
export const ATTR_VALUE = "value";
export const ATTR_ID = "id";
export const ATTR_NONCE = "nonce";

/*partial response types*/
export const XML_TAG_PARTIAL_RESP = "partial-response";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
APPLIED_VST,
ATTR_ID,
ATTR_NAME,
ATTR_NONCE,
ATTR_URL,
ATTR_VALUE,
DEFERRED_HEAD_INSERTS,
Expand Down Expand Up @@ -154,10 +155,16 @@ export class ResponseProcessor implements IResponseProcessor {
/**
* Leaf Tag eval... process whatever is in the eval cdata block
*
* if the eval node carries an explicit nonce attribute (Jakarta Faces 5.0 CSP proposal,
* see https://github.com/jakartaee/faces/issues/1590) it is used, otherwise this falls
* back to the page's own CSP nonce, the same one applied to embedded scripts in update/insert
* blocks, so evaluated code is never silently dropped under a strict CSP script-src policy
*
* @param node the node to eval
*/
eval(node: XMLQuery) {
ExtDomQuery.globalEval(node.cDATAAsString);
const nonce = (node.attr(ATTR_NONCE).value ?? ExtDomQuery.nonce.value) as string;
ExtDomQuery.globalEval(node.cDATAAsString, nonce);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ export class XmlResponses {
</partial-response>
`;

/**
* server-supplied nonce on the eval node itself, per the CSP proposal for
* Jakarta Faces 5.0 (https://github.com/jakartaee/faces/issues/1590)
*/
static EVAL_WITH_EXPLICIT_NONCE = `
<partial-response>
<changes>
<eval nonce="serverSuppliedNonce"><![CDATA[
document.getElementById('evalarea1').innerHTML = 'eval test succeeded';
]]></eval>
</changes>
</partial-response>
`;

static UPDATE_INSERT_1 = `
<partial-response>
<changes>
Expand Down Expand Up @@ -125,6 +139,34 @@ export class XmlResponses {
</partial-response>
`;

/**
* exercises a table with an existing header, footer and 10 rows:
* a single cell replace, a row insert before an existing row,
* a row insert after an existing row, a row delete,
* a header (thead) replace and a footer (tfoot) replace,
* all in one partial response, the way a real jsf response would batch them.
*/
static TABLE_ROW_CELL_HEADER_FOOTER_UPDATE = `
<partial-response>
<changes>
<update id="cell_5"><![CDATA[<td id="cell_5">name5-updated</td>]]></update>
<insert id="insert_before_row2" before="row_2"><![CDATA[
<tr id="row_inserted_before"><td id="cell_inserted_before">insertedBeforeRow2</td><td>extra</td></tr>
]]></insert>
<insert id="insert_after_row7" after="row_7"><![CDATA[
<tr id="row_inserted_after"><td id="cell_inserted_after">insertedAfterRow7</td><td>extra</td></tr>
]]></insert>
<delete id="row_9"></delete>
<update id="dataTable_header"><![CDATA[
<thead id="dataTable_header"><tr id="header_row"><th id="header_cell">NameUpdated</th><th id="header_cell2">ValueUpdated</th></tr></thead>
]]></update>
<update id="dataTable_footer"><![CDATA[
<tfoot id="dataTable_footer"><tr id="footer_row"><td id="footer_cell">FooterUpdated</td><td id="footer_cell2">&nbsp;</td></tr></tfoot>
]]></update>
</changes>
</partial-response>
`;

static VIEWSTATE_1 = `
<partial-response>
<changes>
Expand Down
120 changes: 120 additions & 0 deletions api/src/client/typescript/faces/test/xhrCore/ResponseTest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,88 @@ describe('Tests of the various aspects of the response protocol functionality',

});

it("must handle table cell, row insert/delete and header/footer replacement", function () {
window.document.body.innerHTML = `
<form id="form1" action="boog.html">
<input type="hidden" id="jakarta.faces.ViewState" name="jakarta.faces.ViewState" value="blubbblubblubb"></input>
<table id="dataTable">
<thead id="dataTable_header">
<tr id="header_row">
<th id="header_cell">Name</th>
<th id="header_cell2">Value</th>
</tr>
</thead>
<tbody id="dataTable_body">
<tr id="row_0"><td id="cell_0">name0</td><td id="cell_0b">value0</td></tr>
<tr id="row_1"><td id="cell_1">name1</td><td id="cell_1b">value1</td></tr>
<tr id="row_2"><td id="cell_2">name2</td><td id="cell_2b">value2</td></tr>
<tr id="row_3"><td id="cell_3">name3</td><td id="cell_3b">value3</td></tr>
<tr id="row_4"><td id="cell_4">name4</td><td id="cell_4b">value4</td></tr>
<tr id="row_5"><td id="cell_5">name5</td><td id="cell_5b">value5</td></tr>
<tr id="row_6"><td id="cell_6">name6</td><td id="cell_6b">value6</td></tr>
<tr id="row_7"><td id="cell_7">name7</td><td id="cell_7b">value7</td></tr>
<tr id="row_8"><td id="cell_8">name8</td><td id="cell_8b">value8</td></tr>
<tr id="row_9"><td id="cell_9">name9</td><td id="cell_9b">value9</td></tr>
</tbody>
<tfoot id="dataTable_footer">
<tr id="footer_row">
<td id="footer_cell">Footer</td>
<td id="footer_cell2">&nbsp;</td>
</tr>
</tfoot>
</table>
<input type="button" id="cmd_table_update" value="table update"></input>
</form>`;

expect(DQ.byId("row_0").isPresent(), "sanity check, 10 rows present before the update").to.be.true;
expect(DQ.byId("row_9").isPresent(), "sanity check, 10 rows present before the update").to.be.true;

faces.ajax.request(window.document.getElementById("cmd_table_update"), null, {
execute: "cmd_table_update",
render: "dataTable"
});

this.respond(XmlResponses.TABLE_ROW_CELL_HEADER_FOOTER_UPDATE);

// cell replacement, only the targeted cell changed, the row and the sibling cell survived
expect(DQ.byId("cell_5").innerHTML).to.eq("name5-updated");
expect(DQ.byId("row_5").isPresent()).to.be.true;
expect(DQ.byId("cell_5b").innerHTML).to.eq("value5");

// row delete
expect(DQ.byId("row_9").isAbsent()).to.be.true;

// untouched rows must still be present and unchanged
expect(DQ.byId("row_0").isPresent()).to.be.true;
expect(DQ.byId("cell_0").innerHTML).to.eq("name0");

// row insert before an existing row, order must be preserved
expect(DQ.byId("row_inserted_before").isPresent()).to.be.true;
expect(DQ.byId("row_2").isPresent()).to.be.true;
let bodyHtml = DQ.byId(document.body).html().value as string;
let posInsertedBefore = bodyHtml.indexOf("insertedBeforeRow2");
let posRow1 = bodyHtml.indexOf("id=\"row_1\"");
let posRow2 = bodyHtml.indexOf("id=\"row_2\"");
expect(posRow1 < posInsertedBefore && posInsertedBefore < posRow2).to.be.true;

// row insert after an existing row, order must be preserved
expect(DQ.byId("row_inserted_after").isPresent()).to.be.true;
let posInsertedAfter = bodyHtml.indexOf("insertedAfterRow7");
let posRow7 = bodyHtml.indexOf("id=\"row_7\"");
let posRow8 = bodyHtml.indexOf("id=\"row_8\"");
expect(posRow7 < posInsertedAfter && posInsertedAfter < posRow8).to.be.true;

// header (thead) replacement
expect(DQ.byId("header_cell").innerHTML).to.eq("NameUpdated");
expect(DQ.byId("header_cell2").innerHTML).to.eq("ValueUpdated");

// footer (tfoot) replacement
expect(DQ.byId("footer_cell").innerHTML).to.eq("FooterUpdated");

// the table element itself must not have been replaced wholesale, still exists exactly once
expect(DQ.querySelectorAll("#dataTable").length).to.eq(1);
});

it("must have processed a proper eval of a script given in the eval tag", function () {
DQ.byId("cmd_eval").click();
this.respond(XmlResponses.EVAL_1);
Expand All @@ -238,6 +320,44 @@ describe('Tests of the various aspects of the response protocol functionality',

});

it("must forward an explicit nonce attribute on the eval node to the generated script element", function () {
const createElementSpy = sinon.spy(document, "createElement");

DQ.byId("cmd_eval").click();
this.respond(XmlResponses.EVAL_WITH_EXPLICIT_NONCE);

const evalScript: any = createElementSpy.returnValues.find((el: any) =>
el?.tagName === "SCRIPT" && (el.innerHTML ?? "").indexOf("eval test succeeded") != -1);

expect(evalScript, "the eval code must have run through a generated script element").to.exist;
expect(evalScript.nonce).to.eq("serverSuppliedNonce");

createElementSpy.restore();
});

it("must fall back to the page's own CSP nonce when the eval node carries none", function () {
// window.myfaces is a shared module-level singleton, not per-jsdom-window state,
// so this mutation must be restored or it leaks into unrelated tests later in the run
const originalConfig = window.myfaces.config;
window.myfaces.config = {...window.myfaces.config, cspMeta: {nonce: "fallbackNonce"}};

const createElementSpy = sinon.spy(document, "createElement");

try {
DQ.byId("cmd_eval").click();
this.respond(XmlResponses.EVAL_1);

const evalScript: any = createElementSpy.returnValues.find((el: any) =>
el?.tagName === "SCRIPT" && (el.innerHTML ?? "").indexOf("eval test succeeded") != -1);

expect(evalScript, "the eval code must have run through a generated script element").to.exist;
expect(evalScript.nonce).to.eq("fallbackNonce");
} finally {
createElementSpy.restore();
window.myfaces.config = originalConfig;
}
});

it("must have updated the viewstates properly", function (done) {
DQ.byId("cmd_eval").click();
/*js full submit form, coming from the integration tests*/
Expand Down
Loading
Loading