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
2 changes: 2 additions & 0 deletions changelog.d/lucee-7-0-5-41.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Updated Lucee to 7.0.5.41. New apps from `wheels new`, the starter app and the framework's own `lucee.json` now pin Lucee 7.0.5.41 (previously 7.0.0.395), and the Lucee 7 test image and CI lanes run 7.0.5.41 (previously 7.0.1.100). Lucee 7.0.5.41's Express bundle ships Tomcat 11.0.22. Existing apps keep the version in their own `lucee.json`; change `lucee.version` to `7.0.5.41` to update.
- On Lucee 7.0.5 and later, HTML that view helpers encode (`encode=true` or `encode="attributes"`) uses fewer character references. Lucee's ESAPI extension 3.x encodes only the characters that matter in each context, so `/`, `:`, `(`, `)`, `[`, `]` and spaces stay literal and a double quote becomes `&#34;` rather than `&quot;`; earlier Lucee releases encode them as `&#x2f;`, `&#x3a;` and so on. `<`, `&` and quotes are still encoded wherever they were, and browsers render both forms the same. Its canonicalization also keeps a backslash that precedes a quote (`\"`) instead of dropping it, so such a value is rendered as written. Tests that compare helper output against hard-coded encoded strings may need updating.
2 changes: 1 addition & 1 deletion cli/lucli/templates/app/lucee.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "{{appName}}",
"lucee": {
"version": "7.0.0.395"
"version": "7.0.5.41"
},
"port": {{port}},
"shutdownPort": {{shutdownPort}},
Expand Down
2 changes: 1 addition & 1 deletion compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ services:
build:
context: ./
dockerfile: ./tools/docker/lucee7/Dockerfile
image: wheels-test-lucee7:v1.0.0
image: wheels-test-lucee7:v1.0.1
environment:
# The constructor test-context gate honours WHEELS_ENV;
# the core suite runs in development (config/environment.cfm). Default to
Expand Down
2 changes: 1 addition & 1 deletion examples/starter-app/lucee.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "wheels-starter-app",
"lucee": {
"version": "7.0.0.395"
"version": "7.0.5.41"
},
"port": 8080,
"shutdownPort": 8081,
Expand Down
2 changes: 1 addition & 1 deletion lucee.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "wheels",
"lucee": {
"version": "7.0.0.395"
"version": "7.0.5.41"
},
"port": 8080,
"shutdownPort": 8081,
Expand Down
4 changes: 2 additions & 2 deletions tools/ci/iis/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ if ($p.ExitCode -notin 0, 3010) { throw "URL Rewrite install failed: $($p.ExitCo

# 3. Lucee Express (Tomcat). The AJP connector on 8009 is enabled in its server.xml.
$luceeZip = Join-Path $work 'lucee-express.zip'
Get-Pinned 'https://cdn.lucee.org/lucee-express-7.0.1.100.zip' `
'e2f995d8993747dc782a1c50899aeca40db3cf8993eaf025f8adc61c7dce87e9' $luceeZip
Get-Pinned 'https://cdn.lucee.org/lucee-express-7.0.5.41.zip' `
'30d79340bbefeb36e162530ad1cd6c9d88ff6d29e7c517ebc60fbab06a90bf44' $luceeZip
Expand-Archive -Path $luceeZip -DestinationPath 'C:\lucee' -Force
$serverXml = 'C:\lucee\conf\server.xml'
$xml = Get-Content $serverXml -Raw
Expand Down
2 changes: 1 addition & 1 deletion tools/ci/lucee.ci.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "wheels-ci",
"lucee": {
"version": "7.0.1.100"
"version": "7.0.5.41"
},
"port": 30007,
"shutdownPort": 30008,
Expand Down
2 changes: 1 addition & 1 deletion tools/docker/github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ strategy:
|--------|----------------|------|--------------|
| Lucee 5 | `wheels-lucee5-1` | 60005 | `wheels-test-lucee5:v1.0.2` |
| Lucee 6 | `wheels-lucee6-1` | 60006 | `wheels-test-lucee6:v1.0.2` |
| Lucee 7 | `wheels-lucee7-1` | 60007 | `wheels-test-lucee7:v1.0.0` |
| Lucee 7 | `wheels-lucee7-1` | 60007 | `wheels-test-lucee7:v1.0.1` |
| Adobe 2018 | `wheels-adobe2018-1` | 62018 | `wheels-test-adobe2018:v1.0.2` |
| Adobe 2021 | `wheels-adobe2021-1` | 62021 | `wheels-test-adobe2021:v1.0.2` |
| Adobe 2023 | `wheels-adobe2023-1` | 62023 | `wheels-test-adobe2023:v1.0.1` |
Expand Down
2 changes: 1 addition & 1 deletion tools/docker/lucee7/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ ENV ENV_MODE "remote"
ENV BOX_SERVER_CFCONFIGFILE "/wheels-test-suite/CFConfig.json"
ENV BOX_SERVER_PROFILE "none"
ENV BOX_INSTALL TRUE
ENV BOX_SERVER_APP_CFENGINE "lucee@7.0.1+100"
ENV BOX_SERVER_APP_CFENGINE "lucee@7.0.5+41"

# Set working directory
WORKDIR ${APP_DIR}
Expand Down
2 changes: 1 addition & 1 deletion tools/docker/lucee7/server.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
}
},
"app":{
"cfengine":"lucee@7.0.1+100",
"cfengine":"lucee@7.0.5+41",
"libDirs":"/opt/wheels-lib"
},
"cfconfigFile":"CFConfig.json"
Expand Down
235 changes: 235 additions & 0 deletions vendor/wheels/tests/_assets/view/HtmlEncoding.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/**
* Encoder-agnostic helpers for view specs that check HTML encoding.
*
* Engines encode the same input differently. Lucee's ESAPI extension 2.x encodes almost every
* non-alphanumeric character (`/` becomes `&#x2f;`, a space in an attribute `&#x20;`, `"` becomes
* `&quot;`), while the 3.x extension shipped with Lucee 7.0.5 (built on the OWASP Java Encoder)
* encodes only what the context needs (`"` becomes `&#34;`, `/` stays literal). Both are safe.
*
* `normalize()` lets a spec compare output against one literal expectation on either encoder:
* every character reference is rewritten to a single canonical form. Characters that can change
* the meaning of markup (`&`, `<`, `"`, `'`) always stay encoded, so a missing encoding of one of
* them still fails the comparison. `>` is canonicalised to the literal character, because the
* OWASP encoder leaves it literal inside attribute values, where it cannot end the tag; specs that
* need `>` encoded in text assert that separately with `isSafeText()`.
*
* The safety helpers do not use the engine's encoder at all: they parse the generated markup.
*/
component {

/**
* Rewrites every character reference to a canonical form: `&amp;`, `&lt;`, `&quot;` and
* `&#39;` for the markup-significant characters, the literal character for everything else.
*/
public string function normalize(required string html) {
var rv = "";
var pos = 1;
var total = Len(arguments.html);
while (pos <= total) {
var found = REFind("&(##[xX][0-9a-fA-F]+|##[0-9]+|[a-zA-Z][a-zA-Z0-9]*);", arguments.html, pos, true);
if (found.pos[1] == 0) {
rv &= Mid(arguments.html, pos, total - pos + 1);
break;
}
if (found.pos[1] > pos) {
rv &= Mid(arguments.html, pos, found.pos[1] - pos);
}
var reference = Mid(arguments.html, found.pos[1], found.len[1]);
var codePoint = $codePoint(Mid(arguments.html, found.pos[2], found.len[2]));
rv &= codePoint == 0 ? reference : $canonical(codePoint);
pos = found.pos[1] + found.len[1];
}
return rv;
}

/**
* Fully decodes the character references in a string (the value a browser would see).
*/
public string function decode(required string value) {
var rv = "";
var pos = 1;
var total = Len(arguments.value);
while (pos <= total) {
var found = REFind("&(##[xX][0-9a-fA-F]+|##[0-9]+|[a-zA-Z][a-zA-Z0-9]*);", arguments.value, pos, true);
if (found.pos[1] == 0) {
rv &= Mid(arguments.value, pos, total - pos + 1);
break;
}
if (found.pos[1] > pos) {
rv &= Mid(arguments.value, pos, found.pos[1] - pos);
}
var codePoint = $codePoint(Mid(arguments.value, found.pos[2], found.len[2]));
rv &= codePoint == 0 ? Mid(arguments.value, found.pos[1], found.len[1]) : Chr(codePoint);
pos = found.pos[1] + found.len[1];
}
return rv;
}

/**
* Returns the double-quoted attributes of every tag in `html`, in order, as structs with the
* attribute `name` and its `raw` (still encoded) value. An attribute that broke out of its
* quotes shows up here as an attribute of its own.
*/
public array function attributes(required string html) {
var rv = [];
var pos = 1;
while (true) {
var found = REFind("\s([a-zA-Z_:][-a-zA-Z0-9_:.]*)=""([^""]*)""", arguments.html, pos, true);
if (found.pos[1] == 0) {
break;
}
ArrayAppend(rv, {
name = LCase(Mid(arguments.html, found.pos[2], found.len[2])),
raw = found.len[3] > 0 ? Mid(arguments.html, found.pos[3], found.len[3]) : ""
});
pos = found.pos[1] + found.len[1];
}
return rv;
}

/**
* Every opening `tag` in `html` (e.g. each `<input ...>`), in order. A literal `>` inside a
* quoted attribute value does not end the tag.
*/
public array function tags(required string html, required string tag) {
return REMatchNoCase("<#arguments.tag#\b(?:[^>""]|""[^""]*"")*>", arguments.html);
}

/**
* Whether any tag in `html` carries an attribute called `name`.
*/
public boolean function hasAttribute(required string html, required string name) {
for (var attribute in attributes(arguments.html)) {
if (attribute.name == LCase(arguments.name)) {
return true;
}
}
return false;
}

/**
* The raw (still encoded) value of the first attribute called `name`, or "" when there is none.
*/
public string function attributeValue(required string html, required string name) {
for (var attribute in attributes(arguments.html)) {
if (attribute.name == LCase(arguments.name)) {
return attribute.raw;
}
}
return "";
}

/**
* The raw text between the first opening `tag` and the next `<`, or "" when the tag is absent.
* Encoded text never contains a literal `<`, so this is the whole encoded text node. A literal
* `>` inside a quoted attribute value does not end the tag.
*/
public string function innerText(required string html, required string tag) {
var found = REFindNoCase("<#arguments.tag#\b(?:[^>""]|""[^""]*"")*>([^<]*)", arguments.html, 1, true);
if (found.pos[1] == 0 || found.len[2] == 0) {
return "";
}
return Mid(arguments.html, found.pos[2], found.len[2]);
}

/**
* Whether a raw attribute value is safe inside double quotes: no literal `"`, `'` or `<`.
* A literal `>` is allowed, as it cannot end a quoted attribute value.
*/
public boolean function isSafeAttributeValue(required string raw) {
return !Find("""", arguments.raw) && !Find("'", arguments.raw) && !Find("<", arguments.raw);
}

/**
* Whether raw text content carries no literal `<`, `>`, `"` or `'`.
*/
public boolean function isSafeText(required string raw) {
return !Find("<", arguments.raw) && !Find(">", arguments.raw) && !Find("""", arguments.raw) && !Find("'", arguments.raw);
}

/**
* Checks that `input`, rendered into the `name` attribute of `html`, stayed one attribute value:
* no attribute the input tried to inject (`onclick=` in the input) appears on any tag, the value
* carries no literal `"`, `'` or `<`, and it decodes back to `input`. Returns "" when all hold,
* otherwise a description of the first problem.
*/
public string function attributeProblem(required string html, required string name, required string input) {
for (var injected in REMatch("[a-zA-Z][-a-zA-Z0-9]*=", arguments.input)) {
var injectedName = Left(injected, Len(injected) - 1);
if (hasAttribute(arguments.html, injectedName)) {
return "the input added a [#injectedName#] attribute: #arguments.html#";
}
}
if (!hasAttribute(arguments.html, arguments.name)) {
return "no [#arguments.name#] attribute in: #arguments.html#";
}
var raw = attributeValue(arguments.html, arguments.name);
if (!isSafeAttributeValue(raw)) {
return "[#arguments.name#] value has a literal quote or <: #raw#";
}
if (Compare(decode(raw), arguments.input) != 0) {
return "[#arguments.name#] value decodes to [#decode(raw)#], not the input [#arguments.input#]";
}
return "";
}

/**
* Checks that the raw text node `raw` encodes `input` safely: no literal `<`, `>`, `"` or `'`,
* and it decodes back to `input`. Returns "" when both hold, otherwise a description.
*/
public string function textProblem(required string raw, required string input) {
if (!isSafeText(arguments.raw)) {
return "text has a literal <, >, quote or apostrophe: #arguments.raw#";
}
if (Compare(decode(arguments.raw), arguments.input) != 0) {
return "text decodes to [#decode(arguments.raw)#], not the input [#arguments.input#]";
}
return "";
}

/**
* The code point of a character reference body (`#x2f`, `#47`, `quot`), or 0 when the named
* reference is not one this helper knows.
*/
public numeric function $codePoint(required string body) {
if (Left(arguments.body, 2) == "##x" || Left(arguments.body, 2) == "##X") {
return InputBaseN(Mid(arguments.body, 3, Len(arguments.body) - 2), 16);
}
if (Left(arguments.body, 1) == "##") {
return Val(Mid(arguments.body, 2, Len(arguments.body) - 1));
}
switch (LCase(arguments.body)) {
case "quot":
return 34;
case "apos":
return 39;
case "amp":
return 38;
case "lt":
return 60;
case "gt":
return 62;
case "nbsp":
return 160;
}
return 0;
}

/**
* The canonical form of one decoded character.
*/
public string function $canonical(required numeric codePoint) {
switch (arguments.codePoint) {
case 38:
return "&amp;";
case 60:
return "&lt;";
case 34:
return "&quot;";
case 39:
return "&##39;";
}
return Chr(arguments.codePoint);
}

}
12 changes: 7 additions & 5 deletions vendor/wheels/tests/specs/view/assetsSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ component extends="wheels.WheelsTest" {

function run() {

enc = new wheels.tests._assets.view.HtmlEncoding()

g = application.wo

describe("Tests that assetDomain", () => {
Expand Down Expand Up @@ -352,23 +354,23 @@ component extends="wheels.WheelsTest" {
result = _controller.styleSheetLinkTag(argumentcollection = args)
expected = '<link href="#webPath#stylesheets&##x2f;test.css" media="all" rel="stylesheet" type="text&##x2f;css">#Chr(10)#<link href="#webPath#stylesheets&##x2f;test.css" media="all" rel="stylesheet" type="text&##x2f;css">#Chr(10)#<link href="#webPath#stylesheets&##x2f;jquery.dataTables.min.css" media="all" rel="stylesheet" type="text&##x2f;css">#Chr(10)#<link href="#webPath#stylesheets&##x2f;jquery.dataTables.min.css" media="all" rel="stylesheet" type="text&##x2f;css">#Chr(10)#'

expect(result).toBe(expected)
expect(enc.normalize(result)).toBe(enc.normalize(expected))
})

it("no automatic extension when cfm", () => {
args.source = "http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/start/jquery-ui.css,test.css,https://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/start/jquery-ui.css"
result = _controller.styleSheetLinkTag(argumentcollection = args)
expected = '<link href="http&##x3a;&##x2f;&##x2f;ajax.googleapis.com&##x2f;ajax&##x2f;libs&##x2f;jqueryui&##x2f;1.7.2&##x2f;themes&##x2f;start&##x2f;jquery-ui.css" media="all" rel="stylesheet" type="text&##x2f;css">#Chr(10)#<link href="#webPath#stylesheets&##x2f;test.css" media="all" rel="stylesheet" type="text&##x2f;css">#Chr(10)#<link href="https&##x3a;&##x2f;&##x2f;ajax.googleapis.com&##x2f;ajax&##x2f;libs&##x2f;jqueryui&##x2f;1.7.2&##x2f;themes&##x2f;start&##x2f;jquery-ui.css" media="all" rel="stylesheet" type="text&##x2f;css">#Chr(10)#'

expect(result).toBe(expected)
expect(enc.normalize(result)).toBe(enc.normalize(expected))
})

it("supports external links", () => {
args.source = "http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/start/jquery-ui.css,test.css,https://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/start/jquery-ui.css"
result = _controller.styleSheetLinkTag(argumentcollection = args)
expected = '<link href="http&##x3a;&##x2f;&##x2f;ajax.googleapis.com&##x2f;ajax&##x2f;libs&##x2f;jqueryui&##x2f;1.7.2&##x2f;themes&##x2f;start&##x2f;jquery-ui.css" media="all" rel="stylesheet" type="text&##x2f;css">#Chr(10)#<link href="#webPath#stylesheets&##x2f;test.css" media="all" rel="stylesheet" type="text&##x2f;css">#Chr(10)#<link href="https&##x3a;&##x2f;&##x2f;ajax.googleapis.com&##x2f;ajax&##x2f;libs&##x2f;jqueryui&##x2f;1.7.2&##x2f;themes&##x2f;start&##x2f;jquery-ui.css" media="all" rel="stylesheet" type="text&##x2f;css">#Chr(10)#'

expect(result).toBe(expected)
expect(enc.normalize(result)).toBe(enc.normalize(expected))
})

it("allows specification of delimeter", () => {
Expand All @@ -377,7 +379,7 @@ component extends="wheels.WheelsTest" {
result = _controller.styleSheetLinkTag(argumentcollection = args)
expected = '<link href="#webPath#stylesheets&##x2f;test.css" media="all" rel="stylesheet" type="text&##x2f;css">#Chr(10)#<link href="#webPath#stylesheets&##x2f;test.css" media="all" rel="stylesheet" type="text&##x2f;css">#Chr(10)#<link href="http&##x3a;&##x2f;&##x2f;fonts.googleapis.com&##x2f;css&##x3f;family&##x3d;Istok&##x2b;Web&##x3a;400,700" media="all" rel="stylesheet" type="text&##x2f;css">#Chr(10)#'

expect(result).toBe(expected)
expect(enc.normalize(result)).toBe(enc.normalize(expected))
})

it("type media arguments", () => {
Expand All @@ -387,7 +389,7 @@ component extends="wheels.WheelsTest" {
result = _controller.styleSheetLinkTag(argumentcollection = args)
expected = '<link href="#webPath#stylesheets&##x2f;test.css" rel="stylesheet">#Chr(10)#'

expect(result).toBe(expected)
expect(enc.normalize(result)).toBe(enc.normalize(expected))
})

it("no encoding", () => {
Expand Down
Loading
Loading