utils.js
// utils.js/
var nodemailer = require('nodemailer'),
fs = require('fs'),
nconf = require('nconf'),
Promise = require('bluebird'),
url = require('url'),
pagination = require('pagination'),
logger = require('./utils/logger'),
bases = require('bases'),
path = require('path'),
imageType = require('image-type'),
isSvg = require('is-svg'),
i18n = require('i18n-2'),
request = require('request'),
childProcess = Promise.promisifyAll(require('child_process')),
optipng = require('optipng-bin'),
phantomjs = require('phantomjs-prebuilt'),
surveyIdEncr = nconf.get('app').surveyIdEncr,
smtpConfig = nconf.get('smtp'),
fileType = require('file-type'),
readChunk = require('read-chunk'),
tmp = require('tmp'),
sharp,
Jimp,
selectAnImageSVG = '<svg xmlns="http://www.w3.org/2000/svg" height="150px" width="150px" version="1.0" viewBox="-300 -300 600 600" xml:space="preserve"><circle stroke="#AAA" stroke-width="10" r="280" fill="#FFF"/><text style="letter-spacing:1;text-anchor:middle;text-align:center;stroke-opacity:.5;stroke:#000;stroke-width:2;fill:#444;font-size:360px;font-family:Bitstream Vera Sans,Liberation Sans, Arial, sans-serif;line-height:125%;writing-mode:lr-tb;" transform="scale(.2)">{INNER_TEXT}</text></svg>',
selectAnImageSVGInnerTextOneLine = '<tspan y="180" x="8">{LINE_1}</tspan>',
selectAnImageSVGInnerTextTwoLines = '<tspan y="-40" x="8">{LINE_1}</tspan><tspan y="400" x="8">{LINE_2}</tspan>';
try {
require.resolve('sharp');
sharp = require('sharp');
} catch(e) {
Jimp = require('jimp');
}
// Add "allSettled" for Bluebird, like "all" but waits for all promises to finish
// before throwing any error (first one found is thrown)
Promise.allSettled = function(promises) {
return Promise.all(promises.map(function(promise) {
return promise.reflect();
})).then(function(inspections) {
var results = [];
for (let i = 0, iLen = inspections.length; i<iLen; i++) {
if (!inspections[i].isFulfilled()) {
throw inspections[i].reason();
}
results.push(inspections[i].value());
}
return results;
});
};
function langToWebLocaleIsoInner(lang) {
switch(lang) {
case 'en':
// We use United Kingdom as reference for english
return 'gb';
case 'gl':
// Galician language iso code is different from the region iso code
return 'es-ga';
default:
return lang;
}
};
var localeFiles = fs.readdirSync('locales'),
locales = [],
localesWithIsos = [];
for (var i = 0, len = localeFiles.length; i<len; i++) {
var file = localeFiles[i];
if (/\.json$/.test(file)) {
var lang = file.replace(/\.json$/, "");
locales.push(lang);
localesWithIsos.push({
locale: lang,
iso: langToWebLocaleIsoInner(lang)
});
}
}
function takeSnapshotRaw(url, imgPath, width, height, wait, minSize, tries) {
tries = (tries) ? tries : 0;
var childArgs = [
'--ignore-ssl-errors=true',
path.join(__dirname, 'scripts' + path.sep + 'phantomjs-survey_snapshot.js'),
url,
imgPath,
width,
height,
wait
];
return childProcess.execFileAsync(phantomjs.path, childArgs).then(function(stdout) {
if (!minSize || fs.statSync(imgPath).size >= minSize) {
return Promise.resolve(imgPath);
} else {
// If the image file is very small, it probably wasn't well rendered,
if (tries >= 3) {
// If we already tried the process at least 3 times, we simply inform of the anomaly
logger.warn('Snapshot from url ' + url + ' has an unusual small size and could be wrong.');
return Promise.resolve(imgPath);
} else {
// We retry the process (2 more times at most)
logger.debug('Snapshot from url ' + url + ' seems to be wrong. Retrying...');
return takeSnapshotRaw(url, imgPath, width, height, wait, minSize, tries + 1);
}
}
});
}
function transformImageJimp(input, width, height, upscale, format) {
return Jimp.read(input).then(function(image) {
var w = (width && (upscale || width < image.bitmap.width)) ? width : Jimp.AUTO,
h = (height && (upscale || height < image.bitmap.height)) ? height : Jimp.AUTO;
if (w !== Jimp.AUTO || h !== Jimp.AUTO) {
image = image.resize(w, h);
}
return image;
});
}
function transformImageSharp(input, width, height, upscale, format) {
var promise = sharp(input);
return promise.metadata().then(function(metadata) {
var w = (width && (upscale || width < metadata.width)) ? width : null,
h = (height && (upscale || height < metadata.height)) ? height : null,
options = {
fit: 'inside'
};
if (w || h) {
// Don't crop the image when resizing it by both dimensions
if (w && h) {
options.fit = 'fill';
}
promise = promise.resize(w, h, options);
}
if (!format && !sharp.format[metadata.format].output.buffer) {
format = 'png';
}
if (format) {
promise = promise.toFormat(format);
}
return promise;
});
}
module.exports = function(app) {
Utils = {
encryptSurveyId: function(id) {
return (id) ? bases.toBase(id * surveyIdEncr.factor, surveyIdEncr.base ) : null;
},
decryptSurveyId: function(encrId) {
for (var i=0, len=encrId.length; i<len; i++) {
if (bases.KNOWN_ALPHABETS[surveyIdEncr.base].indexOf(encrId[i]) === -1) {
return null;
}
}
return (encrId) ? bases.fromBase(encrId, surveyIdEncr.base) / surveyIdEncr.factor : null;
},
sendMail: function(mail) {
if (typeof mail.from === 'undefined') {
mail.from = smtpConfig.from;
}
var transporter = nodemailer.createTransport({
host: smtpConfig.host,
port: smtpConfig.port,
ignoreTLS: true,
tls: {
rejectUnauthorized: false
},
secure: true,
auth: {
user: smtpConfig.user,
pass: smtpConfig.pass
}
});
return new Promise(function(resolve, reject) {
transporter.sendMail(mail, function(error, info){
if (error){
logger.error('SMTP error: ' + error);
return reject(error);
}
return resolve();
});
});
},
getPaginationBasePath: function(req) {
var dir = url.parse(req.url).pathname + '?';
for (var i in req.query) {
if (i !== 'page') {
dir += i + '=' + req.query[i] + '&';
}
}
return dir;
},
getPaginationTranslations: function(req) {
return {
'NEXT': req.i18n.__('pagination_next'),
'PREVIOUS': req.i18n.__('pagination_previous'),
'FIRST': req.i18n.__('pagination_first'),
'LAST': req.i18n.__('pagination_last')
};
},
paginationTemplate: function(elementName) {
return function(result) {
function getPrevNextLink(prelink, url, next) {
return '<li class="pagination-' + (next ? 'next' : 'previous') + (url ? '' : ' disabled') + '"><a' + (url ? ' href="' + prelink + url + '"' : '') + '><span class="glyphicon glyphicon-menu-' + (next ? 'right' : 'left') + '"></span></a></li>';
}
var i, len, prelink;
var html = '<div class="col-xs-12 pagination-container"><ul class="pagination">';
if (result.pageCount < 2) {
html += '</ul><span class="col-xs-12 pagination-totals">' + result.totalResult + ' ' + elementName + '</span></div>';
return html;
}
prelink = this.preparePreLink(result.prelink);
html += getPrevNextLink(prelink, result.previous, false);
if (result.previous && result.range[0] !== result.first) {
html += '<li class="pagination-first"><a href="' + prelink + result.first + '">' + result.first + '</a></li>' +
(result.range[0] - 1 !== result.first ? '<li class="pagination-more-before disabled"><a></a></li>' : '');
}
if (result.range.length) {
for( i = 0, len = result.range.length; i < len; i++) {
html += '<li'
if (result.range[i] === result.current) {
html += ' class="active"';
}
html += '><a href="' + prelink + result.range[i] + '">' + result.range[i] + '</a></li>';
}
}
if (result.next && result.range[result.range.length - 1] !== result.last) {
html += (result.range[result.range.length - 1] + 1 !== result.last ? '<li class="pagination-more-after disabled"><a></a></li>' : '') +
'<li class="pagination-last' + (result.next ? '' : ' disabled') + '"><a href="' + prelink + result.last + '">' + result.last + '</a></li>';
}
html += getPrevNextLink(prelink, result.next, true);
if (elementName) {
html += '</ul><span class="col-xs-12 pagination-totals">' + result.totalResult + ' ' + elementName + '</span></div>';
}
return html;
};
},
getPaginationHtml: function(req, pageNr, pageSize, totalResults, elementName) {
return new pagination.TemplatePaginator({
prelink: Utils.getPaginationBasePath(req),
current: pageNr,
rowsPerPage: pageSize,
totalResult: totalResults,
translator: function(str) {
return req.i18n.__('pagination_' + str);
},
template: Utils.paginationTemplate(req.i18n.__(elementName))
}).render();
},
copyBodyToLocals: function(req, res) {
Utils.copyAttributes(req.body, res.locals);
},
copyAttributes: function(origin, dest) {
for (var i in origin) {
if ({}.hasOwnProperty.call(origin, i)) {
dest[i] = origin[i];
}
}
return dest;
},
extractProperties: function(object, deleteFields) {
var props = Utils.copyAttributes(object.get(), {});
for (var i = 0, len = deleteFields.length; i<len; i++) {
delete props[deleteFields[i]];
}
return props;
},
takeSnapshot: function(url, imgPath, nativeWidth, nativeHeight, wait, minSize, retries, imgWidth, imgHeight) {
imgWidth = imgWidth ? imgWidth : nativeWidth;
imgHeight = imgHeight ? imgHeight : nativeHeight;
var tmpFile = tmp.fileSync({ postfix: '.png' });
return takeSnapshotRaw(url, tmpFile.name, nativeWidth, nativeHeight, wait, minSize).then(function() {
// Resize the image
return Utils.transformImage(tmpFile.name, imgWidth, imgHeight, false, 'png', imgPath);
}).then(function() {
// Compress the image content
return childProcess.execFileAsync(optipng, [
'-o7',
'-clobber',
imgPath
]);
}).finally(function() {
// Delete the tmp file
tmpFile.removeCallback();
}).return(imgPath).catch(function(err) {
if (retries) {
logger.debug('Retrying snapshot take for url "' + url + '" after error: ' + err);
return Utils.takeSnapshot(url, imgPath, nativeWidth, nativeHeight, wait, minSize, retries - 1, imgWidth, imgHeight);
}
throw err;
});
},
getRandomInt: function(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
getLocalizedSelectAnImageSVG: function(req) {
var innerTexts = req.i18n.__('select_an_image_svg_text').split('\\n');
return selectAnImageSVG.replace('{INNER_TEXT}', (innerTexts.length === 1) ?
selectAnImageSVGInnerTextOneLine.replace('{LINE_1}', innerTexts[0]) :
selectAnImageSVGInnerTextTwoLines.replace('{LINE_1}', innerTexts[0]).replace('{LINE_2}', innerTexts[1]));
},
checkUrlIsImage: function(url) {
if (url.lastIndexOf('http', 0) !== 0) {
url = 'http://' + url;
}
return new Promise(function(resolve, reject) {
request({
url: url,
timeout: 4000,
encoding: null
}, function(err, res, body) {
if (err) {
if (err.code === 'ETIMEDOUT') {
logger.warn('Couldn\'t check whether the URL "' + url + '" is actually an image due to connection timeout. Will accept it as one.');
return resolve()
}
return reject(err);
}
if (imageType(body) !== null || isSvg(body)) {
resolve();
} else {
reject({
message: 'invalid image file.'
});
}
});
});
},
transformNewlinesToHtml: function(text) {
return text.replace(/(?:\r\n|\r|\n)/g, '<br/>');
},
langToWebLocaleIso: function(lang) {
return langToWebLocaleIsoInner(lang);
},
getLocales: function() {
return localesWithIsos;
},
getI18nConfig: function() {
return {
// setup some locales - other locales default to en silently
locales: locales,
defaultLocale: 'en',
extension: '.json',
// set the cookie name
cookieName: 'locale',
// i18n-2 debug messages can clutter the output. Disable them explicitly even in development mode.
devMode: false
};
},
getI18n: function(locale) {
var i18nObject = new i18n(Utils.getI18nConfig());
if (locale) {
i18nObject.setLocale(locale);
}
return i18nObject;
},
getApplicationBaseURL: function() {
return 'https://' + nconf.get('server').domain;
},
getFileMetadata: function(input) {
var metadata = fileType(Buffer.isBuffer(input) ? input : readChunk.sync(input, 0, fileType.minimumBytes));
if (isSvg(Buffer.isBuffer(input) ? input : fs.readFileSync(input))) {
if (metadata === null) {
metadata = { ext: 'svg' };
}
metadata.mime = 'image/svg+xml';
}
return metadata;
},
getFileMimeType: function(input, defaultMime) {
var metadata = Utils.getFileMetadata(input);
return metadata === null ?
(defaultMime ? defaultMime : 'application/octet-stream') : metadata.mime;
},
getFileType: function(input) {
var metadata = Utils.getFileMetadata(input);
return metadata === null ? null : metadata.mime.split('/')[0];
},
isImage: function(input) {
return Utils.getFileType(input) === 'image';
},
isVideo: function(input) {
return Utils.getFileType(input) === 'video';
},
deleteTmpFilesFromRequest: function(req) {
var paths = [];
for (var name in req.files) {
if ({}.hasOwnProperty.call(req.files, name) && fs.existsSync(req.files[name].path)) {
paths.push(req.files[name].path);
}
}
if (paths.length > 0) {
paths.map(fs.unlinkSync);
logger.debug('Successfully deleted the following tmp files: ' + paths.join(' | '));
}
},
dontDeleteTmpFiles: function() {
return function(req, res, next) {
res.__deleteFilesOnFinished = false;
return next();
}
},
transformImage: function(input, width, height, upscale, format, outputFile) {
var imgFormat = format ? format : null;
if (sharp) {
if (imgFormat && imgFormat.lastIndexOf('image/') === 0) {
imgFormat = imgFormat.replace('image/', '');
}
return transformImageSharp(input, width, height, upscale, imgFormat).then(function(result) {
return outputFile ? result.toFile(outputFile) : result.toBuffer();
});
} else {
if (imgFormat && imgFormat.lastIndexOf('image/') === -1) {
imgFormat = 'image/' + imgFormat;
}
return transformImageJimp(input, width, height, upscale, imgFormat).then(function(result) {
return new Promise(function(resolve, reject) {
if (outputFile) {
result.write(outputFile, function(err) {
return err ? reject(err) : resolve();
});
} else {
result.getBuffer(imgFormat || Jimp.AUTO, function(err, buffer) {
return err ? reject(err) : resolve(buffer);
});
}
});
});
}
},
createOrderArray: function(thisFields, model) {
var scopeArray = [];
for (field of thisFields) {
if (model) {
scopeArray.push([model, field]);
} else {
scopeArray.push([field]);
}
}
return scopeArray
}
};
};