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
28 changes: 28 additions & 0 deletions plugins/star-rating/frontend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,41 @@ var STAR_RATING_EXT_TO_MIME = {
(function(plugin) {
plugin.init = function(app) {

var SAFE_PROVIDED_PATH_RE = /^[a-zA-Z0-9\-_./]*$/;
/**
* Restrict a caller-supplied asset path prefix to a same-origin
* relative path, rejecting absolute/protocol-relative URLs and
* backslash tricks browsers normalize into "//host".
* @param {*} rawValue - value of the provided_url query param
* @returns {String} sanitized path, or '' if rawValue is unsafe
*/
function sanitizeProvidedUrl(rawValue) {
if (typeof rawValue !== 'string' || rawValue === '') {
return '';
}
if (rawValue.indexOf('\\') !== -1 || rawValue.indexOf(':') !== -1 || rawValue.indexOf('//') !== -1) {
return '';
}
if (!SAFE_PROVIDED_PATH_RE.test(rawValue)) {
return '';
}
return rawValue;
}

/**
* Method that render ratings popup template
* @param {*} req - Express request object
* @param {*} res - Express response object
*/
function renderPopup(req, res) {
let countlyPath = countlyConfig.path || '';
//If asset prefix path is passed - use that
if (req.query.provided_url) {
var safeProvidedUrl = sanitizeProvidedUrl(req.query.provided_url);
if (safeProvidedUrl) {
countlyPath = safeProvidedUrl;
}
}

if (countlyPath.length > 0 && !countlyPath.startsWith('/')) {
countlyPath = `/${countlyPath}`;
Expand Down
88 changes: 88 additions & 0 deletions plugins/star-rating/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,96 @@ var APP_ID = "";
var APP_KEY = "";
var DEVICE_ID = "123456789";
var WIDGET_ID = "";

/**
* Pull out every locally-referenced asset path (href/src attributes and
* CSS url(...) references) from a rendered feedback-popup page, ignoring
* absolute URLs like the "https://count.ly" footer link.
* @param {string} html - rendered feedback-popup HTML
* @returns {Array} list of path strings found in the markup
*/
function extractLocalAssetPaths(html) {
var paths = [];
var attrRe = /(?:href|src)\s*=\s*["']([^"']+)["']/g;
var urlRe = /url\(\s*['"]?([^'")]+)['"]?\s*\)/g;
var m;
while ((m = attrRe.exec(html)) !== null) {
if (m[1].startsWith('/')) {
paths.push(m[1]);
}
}
while ((m = urlRe.exec(html)) !== null) {
if (m[1].startsWith('/')) {
paths.push(m[1]);
}
}
return paths;
}

describe('Testing Rating plugin', function() {

describe('Feedback popup asset paths with provided_url', function() {
it('should prefix asset paths with countlyConfig.path when provided_url is not passed', function(done) {
request.get('/feedback/rating')
.expect(200)
.end(function(err, res) {
if (err) {
return done(err);
}
var paths = extractLocalAssetPaths(res.text);
paths.length.should.be.above(15);
paths.forEach(function(p) {
p.should.startWith('/');
p.should.not.startWith('//');
});
var dataAttr = res.text.match(/data-countly-path="([^"]*)"/);
should.exist(dataAttr);
dataAttr[1].should.eql('');
done();
});
});

it('should prefix every asset path with provided_url when it has a leading slash', function(done) {
var providedUrl = '/reverse-proxy/countly';
request.get('/feedback/rating?provided_url=' + encodeURIComponent(providedUrl))
.expect(200)
.end(function(err, res) {
if (err) {
return done(err);
}
var paths = extractLocalAssetPaths(res.text);
paths.length.should.be.above(15);
paths.forEach(function(p) {
p.should.startWith(providedUrl + '/');
});
var dataAttr = res.text.match(/data-countly-path="([^"]*)"/);
should.exist(dataAttr);
dataAttr[1].should.eql(providedUrl);
done();
});
});

it('should add a leading slash to provided_url when missing and still prefix every asset path', function(done) {
var providedUrl = 'reverse-proxy/countly';
request.get('/feedback/rating?provided_url=' + encodeURIComponent(providedUrl))
.expect(200)
.end(function(err, res) {
if (err) {
return done(err);
}
var paths = extractLocalAssetPaths(res.text);
paths.length.should.be.above(15);
paths.forEach(function(p) {
p.should.startWith('/' + providedUrl + '/');
});
var dataAttr = res.text.match(/data-countly-path="([^"]*)"/);
should.exist(dataAttr);
dataAttr[1].should.eql('/' + providedUrl);
done();
});
});
});

describe('Get empty widget list', function() {
it('should return 200 and empty widget list', function(done) {
API_KEY_ADMIN = testUtils.get("API_KEY_ADMIN");
Expand Down
Loading