Skip to content

Commit dd4844d

Browse files
authored
[FEATURE] Properties File Escaping (#214)
In serveResources middleware use nonAsciiEscaper processor to escape .properties files.
1 parent f7125e8 commit dd4844d

File tree

6 files changed

+191
-35
lines changed

6 files changed

+191
-35
lines changed

lib/middleware/MiddlewareManager.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,16 @@ class MiddlewareManager {
108108
await this.addMiddleware("discovery", {
109109
mountPath: "/discovery"
110110
});
111-
await this.addMiddleware("serveResources");
111+
await this.addMiddleware("serveResources", {
112+
wrapperCallback: (serveResourcesModule) => {
113+
return ({resources}) => {
114+
return serveResourcesModule({
115+
resources,
116+
tree: this.tree
117+
});
118+
};
119+
}
120+
});
112121
await this.addMiddleware("serveThemes");
113122
await this.addMiddleware("versionInfo", {
114123
mountPath: "/resources/sap-ui-version.json",

lib/middleware/serveIndex.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ const log = require("@ui5/logger").getLogger("server:middleware:serveIndex");
22
const mime = require("mime-types");
33
const parseurl = require("parseurl");
44

5-
const rProperties = /\.properties$/;
6-
75
const KB = 1024;
86
const MB = KB * KB;
97
const GB = KB * KB * KB;
@@ -15,11 +13,7 @@ const GB = KB * KB * KB;
1513
* @returns {string} mime type
1614
*/
1715
function getMimeType(resource) {
18-
if (rProperties.test(resource.getPath())) {
19-
return "text/plain;charset=ISO-8859-1";
20-
} else {
21-
return mime.lookup(resource.getPath()) || "application/octet-stream";
22-
}
16+
return mime.lookup(resource.getPath()) || "application/octet-stream";
2317
}
2418

2519
/**

lib/middleware/serveResources.js

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ function isFresh(req, res) {
2323
* @param {module:@ui5/fs.AbstractReader} resources.all Resource collection which contains the workspace and the project dependencies
2424
* @returns {Function} Returns a server middleware closure.
2525
*/
26-
function createMiddleware({resources}) {
27-
return function serveResources(req, res, next) {
28-
const pathname = parseurl(req).pathname;
29-
resources.all.byPath(pathname).then(function(resource) {
26+
function createMiddleware({resources, tree: project}) {
27+
return async function serveResources(req, res, next) {
28+
try {
29+
const pathname = parseurl(req).pathname;
30+
const resource = await resources.all.byPath(pathname);
3031
if (!resource) { // Not found
3132
next();
3233
return;
@@ -36,22 +37,21 @@ function createMiddleware({resources}) {
3637
log.verbose("\n" + treeify.asTree(resource.getPathTree()));
3738
}
3839

39-
let type;
40-
let charset;
4140
const resourcePath = resource.getPath();
41+
const type = mime.lookup(resourcePath) || "application/octet-stream";
42+
const charset = mime.charset(type);
4243
if (rProperties.test(resourcePath)) {
43-
// Special handling for *.properties files which are encoded with charset ISO-8859-1.
44-
type = "text/plain";
45-
charset = "ISO-8859-1";
46-
} else {
47-
type = mime.lookup(resourcePath) || "application/octet-stream";
44+
// Special handling for *.properties files escape non ascii characters.
45+
const nonAsciiEscaper = require("@ui5/builder").processors.nonAsciiEscaper;
46+
const propertiesFileSourceEncoding = project && project.resources && project.resources.configuration && project.resources.configuration.propertiesFileSourceEncoding;
47+
const encoding = nonAsciiEscaper.getEncodingFromAlias(propertiesFileSourceEncoding || "ISO-8859-1");
48+
await nonAsciiEscaper({
49+
resources: [resource], options: {
50+
encoding
51+
}
52+
});
4853
}
49-
5054
if (!res.getHeader("Content-Type")) {
51-
if (!charset) {
52-
charset = mime.charset(type);
53-
}
54-
5555
res.setHeader("Content-Type", type + (charset ? "; charset=" + charset : ""));
5656
}
5757

@@ -71,7 +71,7 @@ function createMiddleware({resources}) {
7171
// UTF-8 anyways.
7272
// Also, only process .library, *.js and *.json files. Just like it's done in Application-
7373
// and LibraryBuilder
74-
if (charset === "UTF-8" && rReplaceVersion.test(resourcePath)) {
74+
if ((!charset || charset === "UTF-8") && rReplaceVersion.test(resourcePath)) {
7575
if (resource._project) {
7676
stream = stream.pipe(replaceStream("${version}", resource._project.version));
7777
} else {
@@ -80,9 +80,9 @@ function createMiddleware({resources}) {
8080
}
8181

8282
stream.pipe(res);
83-
}).catch((err) => {
83+
} catch (err) {
8484
next(err);
85-
});
85+
}
8686
};
8787
}
8888

test/lib/server/main.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,29 +67,29 @@ test("Get resource from application.a with replaced version placeholder (/versio
6767
});
6868
});
6969

70-
test("Get resource from application.a (/i18n/i18n.properties) with correct charset 'ISO-8859-1'", (t) => {
70+
test("Get resource from application.a (/i18n/i18n.properties) with correct content-type", (t) => {
7171
return request.get("/i18n/i18n.properties").then((res) => {
7272
if (res.error) {
7373
t.fail(res.error.text);
7474
}
7575
t.deepEqual(res.statusCode, 200, "Correct HTTP status code");
76-
t.deepEqual(res.headers["content-type"], "text/plain; charset=ISO-8859-1", "Correct content type and charset");
77-
t.deepEqual(Buffer.from(res.text, "latin1").toString(), "showHelloButtonText=Say Hello!", "Correct response");
76+
t.deepEqual(res.headers["content-type"], "application/octet-stream", "Correct content type");
77+
t.deepEqual(res.body.toString(), "showHelloButtonText=Say Hello!", "Correct response");
7878
});
7979
});
8080

81-
test("Get resource from application.a (/i18n/i18n_de.properties) with correct encoding 'ISO-8859-1'", (t) => {
81+
test("Get resource from application.a (/i18n/i18n_de.properties) with correct content-type'", (t) => {
8282
return request.get("/i18n/i18n_de.properties")
8383
.responseType("arraybuffer")
8484
.then((res) => {
8585
if (res.error) {
8686
t.fail(res.error.text);
8787
}
8888
t.deepEqual(res.statusCode, 200, "Correct HTTP status code");
89-
t.deepEqual(res.headers["content-type"], "text/plain; charset=ISO-8859-1",
90-
"Correct content type and charset");
89+
t.deepEqual(res.headers["content-type"], "application/octet-stream",
90+
"Correct content type");
9191

92-
t.deepEqual(res.body.toString("latin1"), "showHelloButtonText=Say ä!", "Correct response");
92+
t.deepEqual(res.body.toString(), "showHelloButtonText=Say \\u00e4!", "Correct response");
9393
// Because it took so long to figure this out I keep the below line. It is equivalent to the deepEqual above
9494
// t.deepEqual(res.body.toString("latin1"), Buffer.from("showHelloButtonText=Say \u00e4!", "latin1").toString("latin1"),
9595
// "Correct response");

test/lib/server/middleware/serveIndex.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ test.serial("Check if index for files is created", (t) => {
4141
end: function(content) {
4242
t.regex(content, /<td title="1024 Bytes">1\.00 KB<\/td>\s*<td><a href="\/myFile1\.meh">myFile1\.meh<\/a><\/td>\s*<td>application\/octet-stream<\/td>/);
4343
t.regex(content, /<td title="1048576 Bytes">1\.00 MB<\/td>\s*<td><a href="\/myFile2\.js">myFile2\.js<\/a><\/td>\s*<td>application\/javascript<\/td>/g);
44-
t.regex(content, /<td title="1073741824 Bytes">1\.00 GB<\/td>\s*<td><a href="\/myFile3\.properties">myFile3\.properties<\/a><\/td>\s*<td>text\/plain;charset=ISO-8859-1<\/td>/g);
44+
t.regex(content, /<td title="1073741824 Bytes">1\.00 GB<\/td>\s*<td><a href="\/myFile3\.properties">myFile3\.properties<\/a><\/td>\s*<td>application\/octet-stream<\/td>/g);
4545
resolve();
4646
},
4747
};
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
const test = require("ava");
2+
const sinon = require("sinon");
3+
const resourceFactory = require("@ui5/fs").resourceFactory;
4+
const serveResourcesMiddleware = require("../../../../lib/middleware/serveResources");
5+
const writeResource = function(writer, path, size, stringContent) {
6+
const statInfo = {
7+
ino: 0,
8+
ctime: new Date(),
9+
mtime: new Date(),
10+
size: size,
11+
isDirectory: function() {
12+
return false;
13+
}
14+
};
15+
const resource = resourceFactory.createResource({path, buffer: Buffer.from(stringContent, "latin1"), statInfo});
16+
// stub resource functionality in order to be able to get the Resource's content. Otherwise it would be drained.
17+
sinon.stub(resource, "getStream").returns({
18+
pipe: function() {
19+
}
20+
});
21+
return writer.write(resource).then(() => {
22+
return resource;
23+
});
24+
};
25+
const fakeResponse = {
26+
writeHead: function(status, contentType) {},
27+
getHeader: function(string) {},
28+
setHeader: function(string, header) {}
29+
};
30+
31+
test.afterEach.always((t) => {
32+
sinon.restore();
33+
});
34+
35+
36+
test.serial("Check if properties file is served properly", (t) => {
37+
t.plan(4);
38+
39+
const readerWriter = resourceFactory.createAdapter({virBasePath: "/"});
40+
41+
return writeResource(readerWriter, "/myFile3.properties", 1024 * 1024, "key=titel\nfame=straße").then((resource) => {
42+
const setStringSpy = sinon.spy(resource, "setString");
43+
const middleware = serveResourcesMiddleware({
44+
resources: {
45+
all: readerWriter
46+
},
47+
tree: {
48+
resources: {
49+
configuration: {
50+
propertiesFileSourceEncoding: "ISO-8859-1"
51+
}
52+
}
53+
}
54+
});
55+
56+
const response = fakeResponse;
57+
58+
const setHeaderSpy = sinon.spy(response, "setHeader");
59+
const req = {
60+
url: "/myFile3.properties",
61+
headers: {}
62+
};
63+
const next = function(err) {
64+
throw new Error(`Next callback called with error: ${err.message}`);
65+
};
66+
return middleware(req, response, next).then((o) => {
67+
return resource.getString();
68+
}).then((content) => {
69+
t.is(content, `key=titel
70+
fame=stra\\u00dfe`);
71+
t.is(setHeaderSpy.callCount, 2);
72+
t.is(setStringSpy.callCount, 1);
73+
t.is(setHeaderSpy.getCall(0).lastArg, "application/octet-stream");
74+
});
75+
});
76+
});
77+
78+
test.serial("Check if properties file is served properly with UTF-8", (t) => {
79+
t.plan(4);
80+
81+
const readerWriter = resourceFactory.createAdapter({virBasePath: "/"});
82+
83+
return writeResource(readerWriter, "/myFile3.properties", 1024 * 1024, "key=titel\nfame=straße").then((resource) => {
84+
const setStringSpy = sinon.spy(resource, "setString");
85+
const middleware = serveResourcesMiddleware({
86+
resources: {
87+
all: readerWriter
88+
},
89+
tree: {
90+
resources: {
91+
configuration: {
92+
propertiesFileSourceEncoding: "UTF-8"
93+
}
94+
}
95+
}
96+
});
97+
98+
const response = fakeResponse;
99+
100+
const setHeaderSpy = sinon.spy(response, "setHeader");
101+
const req = {
102+
url: "/myFile3.properties",
103+
headers: {}
104+
};
105+
const next = function(err) {
106+
throw new Error(`Next callback called with error: ${err.message}`);
107+
};
108+
return middleware(req, response, next).then((o) => {
109+
return resource.getString();
110+
}).then((content) => {
111+
t.is(content, `key=titel
112+
fame=stra\\ufffde`);
113+
t.is(setHeaderSpy.callCount, 2);
114+
t.is(setStringSpy.callCount, 1);
115+
t.is(setHeaderSpy.getCall(0).lastArg, "application/octet-stream");
116+
});
117+
});
118+
});
119+
120+
test.serial("Check if properties file is served properly without property setting", (t) => {
121+
t.plan(4);
122+
123+
const readerWriter = resourceFactory.createAdapter({virBasePath: "/"});
124+
125+
return writeResource(readerWriter, "/myFile3.properties", 1024 * 1024, "key=titel\nfame=straße").then((resource) => {
126+
const setStringSpy = sinon.spy(resource, "setString");
127+
const middleware = serveResourcesMiddleware({
128+
resources: {
129+
all: readerWriter
130+
}
131+
});
132+
133+
const response = fakeResponse;
134+
135+
const setHeaderSpy = sinon.spy(response, "setHeader");
136+
const req = {
137+
url: "/myFile3.properties",
138+
headers: {}
139+
};
140+
const next = function(err) {
141+
throw new Error(`Next callback called with error: ${err.stack}`);
142+
};
143+
return middleware(req, response, next).then((o) => {
144+
return resource.getString();
145+
}).then((content) => {
146+
t.is(content, `key=titel
147+
fame=stra\\u00dfe`);
148+
t.is(setHeaderSpy.callCount, 2);
149+
t.is(setStringSpy.callCount, 1);
150+
t.is(setHeaderSpy.getCall(0).lastArg, "application/octet-stream");
151+
});
152+
});
153+
});

0 commit comments

Comments
 (0)