Skip to content

Commit d355960

Browse files
Merge remote-tracking branch 'origin/master' into upadte-changelog-for-event-with-ampersand-fix
2 parents a2521fd + d38ff06 commit d355960

File tree

12 files changed

+1448
-76
lines changed

12 files changed

+1448
-76
lines changed

.github/workflows/main.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,13 @@ jobs:
278278
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -O /tmp/chrome.deb
279279
apt install -y /tmp/chrome.deb
280280
281+
- name: Install Sharp dependencies for image processing
282+
shell: bash
283+
run: |
284+
export DEBIAN_FRONTEND=noninteractive
285+
apt-get update -y
286+
apt-get install -y libvips-dev
287+
281288
- name: Copy code
282289
shell: bash
283290
run: cp -rf ./* /opt/countly
@@ -331,6 +338,6 @@ jobs:
331338
working-directory: /opt/countly/ui-tests/cypress
332339
run: |
333340
ARTIFACT_ARCHIVE_NAME="$(date '+%Y%m%d-%H.%M')_${GITHUB_REPOSITORY#*/}_CI#${{ github.run_number }}_${{ matrix.test_type }}.tar.gz"
334-
mkdir -p screenshots videos
335-
tar zcvf "$ARTIFACT_ARCHIVE_NAME" screenshots videos
341+
mkdir -p screenshots videos downloads
342+
tar zcvf "$ARTIFACT_ARCHIVE_NAME" screenshots videos downloads
336343
curl -o /tmp/uploader.log -u "${{ secrets.BOX_UPLOAD_AUTH }}" ${{ secrets.BOX_UPLOAD_PATH }} -T "$ARTIFACT_ARCHIVE_NAME"

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
## Version 25.03.xx
22
Fixes:
33
- [push] Fixed timeout setting
4+
- [security] Fixed injection possibility on res.expose
5+
6+
Enterprise Fixes:
7+
- [groups] Add logs for user updates
8+
- [surveys] Change question map log to debug log
49

510
Enterprise Fixes:
611
- [data-manager] Fixed bug when merging events with ampersand symbol in the name

bin/scripts/device_list/package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/express/app.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,8 +438,10 @@ Promise.all([plugins.dbConnection(countlyConfig), plugins.dbConnection("countly_
438438
next();
439439
});
440440

441-
app.use('*.svg', function(req, res, next) {
442-
res.setHeader('Content-Type', 'image/svg+xml; charset=UTF-8');
441+
app.use(function(req, res, next) {
442+
if (req.path.endsWith('.svg')) {
443+
res.setHeader('Content-Type', 'image/svg+xml; charset=UTF-8');
444+
}
443445
next();
444446
});
445447

frontend/express/libs/express-expose.js

Lines changed: 26 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ function renderNamespace(str) {
157157
function renderObject(obj, namespace) {
158158
return Object.keys(obj).map(function(key) {
159159
var val = obj[key];
160-
return namespace + '["' + key + '"] = ' + string(val) + ';';
160+
return namespace + '["' + escape_js_string(key) + '"] = ' + string(val) + ';';
161161
}).join('\n');
162162
}
163163

@@ -180,61 +180,49 @@ function string(obj) {
180180
}
181181
else if ('[object Object]' === Object.prototype.toString.call(obj)) {
182182
return '{' + Object.keys(obj).map(function(key) {
183-
return '"' + key + '":' + string(obj[key]);
183+
return '"' + escape_js_string(key) + '":' + string(obj[key]);
184184
}).join(', ') + '}';
185185
}
186186
else {
187-
obj = escape_html(JSON.stringify(obj));
187+
obj = JSON.stringify(obj);
188188
if (obj) {
189+
// Only escape things that could break out of script context
189190
obj = obj.replace(/<\/script>/ig, '</scr"+"ipt>');
191+
obj = obj.replace(/<!--/g, '<\\!--');
192+
obj = obj.replace(/\u2028/g, '\\u2028'); // Line separator
193+
obj = obj.replace(/\u2029/g, '\\u2029'); // Paragraph separator
190194
}
191195
return obj;
192196
}
193197
}
194198

195-
var matchHtmlRegExp = /[<>]/;
196-
197199
/**
198-
* Escape special characters in the given string of html.
200+
* Escape special characters that could break JavaScript string context
199201
*
200-
* @param {string} str - The string to escape for inserting into HTML
202+
* @param {string} str - The string to escape
201203
* @return {string} escaped string
202204
* @public
203205
*/
204-
function escape_html(str) {
205-
str = '' + str;
206-
var match = matchHtmlRegExp.exec(str);
207-
208-
if (!match) {
206+
function escape_js_string(str) {
207+
if (typeof str !== 'string') {
209208
return str;
210209
}
211210

212-
var escape;
213-
var html = '';
214-
var index = 0;
215-
var lastIndex = 0;
216-
217-
for (index = match.index; index < str.length; index++) {
218-
switch (str.charCodeAt(index)) {
219-
case 60: // <
220-
escape = '&lt;';
221-
break;
222-
case 62: // >
223-
escape = '&gt;';
224-
break;
225-
default:
226-
continue;
227-
}
228-
229-
if (lastIndex !== index) {
230-
html += str.substring(lastIndex, index);
231-
}
232-
233-
lastIndex = index + 1;
234-
html += escape;
235-
}
236-
237-
return lastIndex !== index ? html + str.substring(lastIndex, index) : html;
211+
return str
212+
.replace(/\\/g, '\\\\') // Backslash
213+
.replace(/"/g, '\\"') // Double quote
214+
.replace(/'/g, "\\'") // Single quote
215+
.replace(/`/g, '\\`') // Backtick (template literal)
216+
.replace(/\$/g, '\\$') // Dollar sign (template literal)
217+
.replace(/\n/g, '\\n') // Newline
218+
.replace(/\r/g, '\\r') // Carriage return
219+
.replace(/\t/g, '\\t') // Tab
220+
.replace(/\f/g, '\\f') // Form feed
221+
.replace(/\v/g, '\\v') // Vertical tab
222+
.replace(/\0/g, '\\0') // Null character
223+
.replace(/[\u0000-\u001F\u007F-\u009F]/g, function(ch) {
224+
return '\\u' + ('0000' + ch.charCodeAt(0).toString(16)).slice(-4);
225+
});
238226
}
239227

240228
exports = module.exports = function(app) {

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
"ejs": "3.1.10",
6060
"errorhandler": "1.5.1",
6161
"express": "4.21.2",
62-
"express-rate-limit": "8.2.0",
62+
"express-rate-limit": "8.2.1",
6363
"express-session": "1.18.2",
6464
"form-data": "^4.0.0",
6565
"formidable": "2.1.3",
@@ -92,7 +92,7 @@
9292
"offline-geocoder": "git+https://github.com/Countly/offline-geocoder.git",
9393
"properties-parser": "0.6.0",
9494
"puppeteer": "^24.6.1",
95-
"sass": "1.93.2",
95+
"sass": "1.93.3",
9696
"semver": "^7.7.1",
9797
"sharp": "^0.34.2",
9898
"sqlite3": "^5.1.7",

ui-tests/cypress.config.sample.js

Lines changed: 116 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
const { defineConfig } = require("cypress");
2-
const fs = require('fs');
2+
const fs = require("fs");
3+
const pdfjsLib = require("pdfjs-dist/legacy/build/pdf.js");
4+
const { PNG } = require("pngjs");
5+
const sharp = require("sharp");
6+
7+
// Define missing DOMMatrix in Node context (for pdfjs)
8+
if (typeof global.DOMMatrix === "undefined") {
9+
global.DOMMatrix = class DOMMatrix { };
10+
}
311

412
module.exports = defineConfig({
513
e2e: {
@@ -14,20 +22,116 @@ module.exports = defineConfig({
1422
watchForFileChanges: true,
1523
video: true,
1624
setupNodeEvents(on, config) {
17-
on('after:spec', (spec, results) => {
18-
if (results && results.video) {
19-
const failures = results.tests.some((test) =>
20-
test.attempts.some((attempt) => attempt.state === 'failed')
21-
);
22-
if (!failures) {
23-
// delete the video if the spec passed and no tests retried
24-
const videoPath = results.video;
25-
if (fs.existsSync(videoPath)) {
26-
fs.unlinkSync(videoPath);
25+
// Task: verify PDF images, logo, and text content
26+
on("task", {
27+
async verifyPdf({ filePath, options = {} }) {
28+
// options: { referenceLogoPath: string }
29+
30+
// Load PDF file
31+
const data = new Uint8Array(fs.readFileSync(filePath));
32+
const pdfDoc = await pdfjsLib.getDocument({ data }).promise;
33+
34+
// Import pixelmatch only if logo check is needed
35+
let pixelmatch;
36+
const doLogoCheck = !!options.referenceLogoPath;
37+
if (doLogoCheck) {
38+
const pm = await import("pixelmatch");
39+
pixelmatch = pm.default;
40+
}
41+
42+
let hasImage = false;
43+
let logoFound = false;
44+
let extractedText = ""; //store text here
45+
46+
// Loop through all pages
47+
for (let p = 1; p <= pdfDoc.numPages; p++) {
48+
const page = await pdfDoc.getPage(p);
49+
50+
//Extract text content from page
51+
const textContent = await page.getTextContent();
52+
const pageText = textContent.items.map((item) => item.str).join(" ");
53+
extractedText += pageText + "\n";
54+
55+
//Check for image operators
56+
const ops = await page.getOperatorList();
57+
58+
for (let i = 0; i < ops.fnArray.length; i++) {
59+
const fn = ops.fnArray[i];
60+
const args = ops.argsArray[i];
61+
62+
if (
63+
fn === pdfjsLib.OPS.paintImageXObject ||
64+
fn === pdfjsLib.OPS.paintJpegXObject ||
65+
fn === pdfjsLib.OPS.paintInlineImageXObject
66+
) {
67+
hasImage = true;
68+
69+
if (doLogoCheck && args[0]) {
70+
const objName = args[0];
71+
const imgData = await page.objs.get(objName);
72+
if (!imgData) {
73+
continue;
74+
}
75+
76+
const pdfImg = new PNG({ width: imgData.width, height: imgData.height });
77+
pdfImg.data = imgData.data;
78+
79+
const pdfBuffer = PNG.sync.write(pdfImg);
80+
const refLogo = PNG.sync.read(fs.readFileSync(options.referenceLogoPath));
81+
82+
const resizedPdfBuffer = await sharp(pdfBuffer)
83+
.resize(refLogo.width, refLogo.height)
84+
.png()
85+
.toBuffer();
86+
87+
const resizedPdfImg = PNG.sync.read(resizedPdfBuffer);
88+
89+
const diff = new PNG({ width: refLogo.width, height: refLogo.height });
90+
const mismatched = pixelmatch(
91+
refLogo.data,
92+
resizedPdfImg.data,
93+
diff.data,
94+
refLogo.width,
95+
refLogo.height,
96+
{ threshold: 0.1 }
97+
);
98+
99+
if (mismatched === 0) {
100+
logoFound = true;
101+
break;
102+
}
103+
}
104+
}
27105
}
106+
107+
if ((doLogoCheck && logoFound) || (!doLogoCheck && hasImage)) {
108+
break;
109+
}
110+
}
111+
112+
if (doLogoCheck && !logoFound) {
113+
throw new Error("Logo in PDF does not match reference image");
114+
}
115+
116+
//Return with extracted text
117+
return {
118+
hasImage,
119+
logoFound,
120+
text: extractedText,
121+
numPages: pdfDoc.numPages
122+
};
123+
},
124+
});
125+
126+
on("after:spec", (spec, results) => {
127+
if (results?.video) {
128+
const hasFailures = results.tests.some((t) => t.attempts.some((a) => a.state === "failed"));
129+
if (!hasFailures && fs.existsSync(results.video)) {
130+
fs.unlinkSync(results.video);
28131
}
29132
}
30133
});
134+
31135
on("before:browser:launch", (browser, launchOptions) => {
32136
if (["chrome", "edge", "electron"].includes(browser.name)) {
33137
if (browser.isHeadless) {
@@ -42,6 +146,4 @@ module.exports = defineConfig({
42146
});
43147
},
44148
},
45-
});
46-
47-
149+
});

0 commit comments

Comments
 (0)