/*global window*/
/**
* Module rendering views as images
* @module api/utils/render
*/
var puppeteer;
try {
puppeteer = require('puppeteer');
}
catch (err) {
if (process.env.COUNTLY_CONTAINER !== 'frontend') {
console.warn(
`Puppeteer not installed. Please install puppeteer if
you would like to use api/utils/render.js. \nGracefully skipping
any functionality associated with Puppeteer...`, err.stack
);
}
}
var pathModule = require('path');
var exec = require('child_process').exec;
var alternateChrome = true;
var chromePath = "";
var countlyFs = require('./countlyFs');
var log = require('./log.js')('core:render');
var countlyConfig = require('./../config', 'dont-enclose');
var fs = require('fs');
/**
* Function to render views as images
* @param {object} options - options required for rendering
* @param {string} options.host - the hostname
* @param {string} options.token - the login token value
* @param {string} options.view - the view to open
* @param {string} options.id - the id of the block to capture screenshot of
* @param {string} options.savePath - path where to save the screenshot
* @param {function} options.cbFn - function called after opening the view
* @param {function} options.beforeScrnCbFn - function called just before capturing the screenshot
* @param {object} options.dimensions - the dimensions of the screenshot
* @param {number} options.dimensions.width - the width of the screenshot
* @param {number} options.dimensions.height - the height of the screenshot
* @param {number} options.dimensions.padding - the padding value to subtract from the height of the screenshot
* @param {number} options.dimensions.scale - the scale(ppi) value of the screenshot
* @param {function} cb - callback function called with the error value or the image data
* @return {void} void
*/
exports.renderView = function(options, cb) {
if (puppeteer === undefined) {
cb = typeof cb === 'function' ? cb : () => undefined;
return cb(new Error(
'Puppeteer not installed. Please install Puppeteer to use this plugin.'
));
}
(async() => {
try {
if (!chromePath && alternateChrome) {
chromePath = await fetchChromeExecutablePath();
}
var settings = {
headless: true,
env: {
//https://github.com/hardkoded/puppeteer-sharp/issues/2633
XDG_CONFIG_HOME: pathModule.resolve(__dirname, "../../.cache/chrome/tmp/.chromium"),
XDG_CACHE_HOME: pathModule.resolve(__dirname, "../../.cache/chrome/tmp/.chromium")
},
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors', '--disable-web-security'],
ignoreHTTPSErrors: true,
userDataDir: pathModule.resolve(__dirname, "../../dump/chrome/" + Date.now())
};
if (chromePath) {
settings.executablePath = chromePath;
}
var browser = await puppeteer.launch(settings);
try {
log.d('Started rendering images');
var page = await browser.newPage();
await page.setBypassCSP(true);
page.on('console', (msg) => {
log.d("Headless chrome page log", msg.text());
});
page.on('pageerror', (error) => {
log.e("Headless chrome page error message", error.message);
});
page.on('response', (response) => {
log.d("Headless chrome page response", response.status(), response.url());
});
page.on('requestfailed', (request) => {
log.d("Headless chrome page failed request", request.failure().errorText, request.url());
});
var host = (process.env.COUNTLY_CONFIG_PROTOCOL || "http") + "://" + (process.env.COUNTLY_CONFIG_HOSTNAME || "localhost") + countlyConfig.path;
if (options.host) {
host = options.host + countlyConfig.path;
}
var token = options.token;
var view = options.view;
var id = options.id;
var path = options.savePath || pathModule.resolve(__dirname, "../../frontend/express/public/images/screenshots/" + "screenshot_" + Date.now() + ".png");
var cbFn = options.cbFn || function() {};
var beforeScrnCbFn = options.beforeScrnCbFn || function() {};
var source = options.source;
var updatedTimeout = options.timeout || 30000;
var waitForRegex = options.waitForRegex;
var waitForRegexAfterCbfn = options.waitForRegexAfterCbfn;
options.dimensions = {
width: options.dimensions && options.dimensions.width ? options.dimensions.width : 1800,
height: options.dimensions && options.dimensions.height ? options.dimensions.height : 0,
padding: options.dimensions && options.dimensions.padding ? options.dimensions.padding : 0,
scale: options.dimensions && options.dimensions.scale ? options.dimensions.scale : 2
};
page.setDefaultNavigationTimeout(updatedTimeout);
const resp = await page.goto(host + '/login/token/' + token + '?ssr=true');
const status = resp?.status();
if (status !== 200) {
throw new Error(`Failed to open login page. Status: ${status}`);
}
await page.waitForSelector('countly', {timeout: updatedTimeout});
await timeout(1500);
await page.goto(host + view);
if (waitForRegex) {
await page.waitForResponse(
function(response) {
var url = response.url();
log.d("waitForRegex - Response Status: " + response.status() + ", URL: " + url);
if (waitForRegex.test(url) && response.status() === 200) {
return true;
}
else {
return false;
}
},
{ timeout: updatedTimeout }
);
}
await timeout(500);
await page.evaluate(cbFn, options);
if (waitForRegexAfterCbfn) {
if (waitForRegex) {
await page.waitForResponse(
function(response) {
var url = response.url();
log.d("waitForRegexAfterCbfn - Response Status: " + response.status() + ", URL: " + url);
if (waitForRegex.test(url) && response.status() === 200) {
return true;
}
else {
return false;
}
},
{ timeout: updatedTimeout }
);
}
}
await timeout(1500);
await page.setViewport({
width: parseInt(options.dimensions.width),
height: parseInt(options.dimensions.height),
deviceScaleFactor: options.dimensions.scale
});
await timeout(1500);
var bodyHandle = await page.$('body');
var dimensions = await bodyHandle.boundingBox();
await page.setViewport({
width: parseInt(options.dimensions.width || dimensions.width),
height: parseInt(dimensions.height - options.dimensions.padding),
deviceScaleFactor: options.dimensions.scale
});
await timeout(1500);
await page.evaluate(beforeScrnCbFn, options);
await timeout(1500);
var image = "";
var screenshotOptions = {
type: 'png',
encoding: 'binary'
};
if (id) {
var rect = await page.evaluate(function(selector) {
/*global document */
var element = document.querySelector(selector);
dimensions = element.getBoundingClientRect();
return {
left: dimensions.x,
top: dimensions.y,
width: dimensions.width,
height: dimensions.height,
id: element.id
};
}, id);
await page.setViewport({
width: options.dimensions.width,
height: parseInt(rect.height),
deviceScaleFactor: options.dimensions.scale
});
var clip = {
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height
};
screenshotOptions.clip = clip;
}
image = await page.screenshot(screenshotOptions);
await saveScreenshot(image, path, source);
await page.evaluate(function() {
var $ = window.$;
$("#user-logout").trigger("click");
});
await timeout(1500);
await bodyHandle.dispose();
await browser.close();
// Remove user data directory after use
fs.rmSync(settings.userDataDir, { recursive: true, force: true });
var imageData = {
image: image,
path: path
};
log.d('Finished rendering images');
return cb(null, imageData);
}
catch (e) {
log.e("Headless chrome browser error", e);
await browser.close();
// Remove user data directory after use
fs.rmSync(settings.userDataDir, { recursive: true, force: true });
return cb(e);
}
}
catch (err) {
if (cb) {
log.e("Headless chrome error", err);
return cb(err);
}
}
})();
};
/**
* Function to fetch Chrome executable
* @returns {Promise} Promise
*/
function fetchChromeExecutablePath() {
return new Promise(function(resolve) {
exec('ls /etc/ | grep -i "redhat-release" | wc -l', function(error1, stdout1, stderr1) {
if (error1 || parseInt(stdout1) !== 1) {
if (stderr1) {
log.e(stderr1);
}
alternateChrome = false;
return resolve();
}
exec('cat /etc/redhat-release | grep -i "release 6" | wc -l', function(error2, stdout2, stderr2) {
if (error2 || parseInt(stdout2) !== 1) {
if (stderr2) {
log.e(stderr2);
}
alternateChrome = false;
return resolve();
}
var path = "/usr/bin/google-chrome-stable";
return resolve(path);
});
});
});
}
/**
* Function to save screenshots
* @param {Buffer} image - image data to store
* @param {String} path - path where image should be stored
* @param {String} source - who provided image
* @returns {Promise} Promise
*/
function saveScreenshot(image, path, source) {
return new Promise(function(resolve) {
var buffer = image;
var saveDataOptions = {writeMode: "overwrite"};
if (source && source.length) {
saveDataOptions.id = source;
}
countlyFs.saveData("screenshots", path, buffer, saveDataOptions, function(err3) {
if (err3) {
log.e(err3, err3.stack);
}
return resolve();
});
});
}
/**
* Function to set a timeout
* @param {number} ms - Total milliseconds
* @returns {Promise} Promise
*/
function timeout(ms) {
return new Promise(function(resolve) {
setTimeout(resolve, ms);
});
}