/**
* @module utils
*/
const crypto = require( 'crypto' );
const config = require( '../models/config-model' ).server;
const EVP_BytesToKey = require( 'evp_bytestokey' );
const validUrl = require( 'valid-url' );
// var debug = require( 'debug' )( 'utils' );
/**
* Returns a unique, predictable openRosaKey from a survey oject
*
* @static
* @param {module:survey-model~SurveyObject} survey - survey object
* @param { string } [prefix] - prefix
* @return {string|null} openRosaKey
*/
function getOpenRosaKey( survey, prefix ) {
if ( !survey || !survey.openRosaServer || !survey.openRosaId ) {
return null;
}
prefix = prefix || 'or:';
// Server URL is not case sensitive, form ID is case-sensitive
return `${prefix + cleanUrl( survey.openRosaServer )},${survey.openRosaId.trim()}`;
}
/**
* Returns a XForm manifest hash.
*
* @static
* @param {Array} manifest - hash of XForm manifest
* @param { string } type - Webform type
* @return { string } Hash
*/
function getXformsManifestHash( manifest, type ) {
const hash = '';
if ( !manifest || manifest.length === 0 ) {
return hash;
}
if ( type === 'all' ) {
return md5( JSON.stringify( manifest ) );
}
if ( type ) {
const filtered = manifest.map( mediaFile => mediaFile[ type ] );
return md5( JSON.stringify( filtered ) );
}
return hash;
}
/**
* Cleans a Server URL so it becomes useful as a db key
* It strips the protocol, removes a trailing slash, removes www, and converts to lowercase
*
* @static
* @param { string } url - Url to be cleaned up
* @return { string } Cleaned up url
*/
function cleanUrl( url ) {
url = url.trim();
if ( url.lastIndexOf( '/' ) === url.length - 1 ) {
url = url.substring( 0, url.length - 1 );
}
const matches = url.match( /https?:\/\/(www\.)?(.+)/ );
if ( matches && matches.length > 2 ) {
return matches[ 2 ].toLowerCase();
}
return url;
}
/**
* The name of this function is deceiving. It checks for a valid server URL and therefore doesn't approve of:
* - fragment identifiers
* - query strings
*
* @static
* @param { string } url - Url to be validated
* @return { boolean } Whether the url is valid
*/
function isValidUrl( url ) {
return !!validUrl.isWebUri( url ) && !( /\?/.test( url ) ) && !( /#/.test( url ) );
}
/**
* Returns md5 hash of given message
*
* @static
* @param { string } message - Message to be hashed
* @return { string } Hash string
*/
function md5( message ) {
const hash = crypto.createHash( 'md5' );
hash.update( message );
return hash.digest( 'hex' );
}
/**
* This is not secure encryption as it doesn't use a random cipher. Therefore the result is
* always the same for each text & pw (which is desirable in this case).
* This means the password is vulnerable to be cracked,
* and we should use a dedicated low-importance password for this.
*
* @static
* @param { string } text - The text to be encrypted
* @param { string } pw - The password to use for encryption
* @return { string } The encrypted result
*/
function insecureAes192Encrypt( text, pw ) {
let encrypted;
const stuff = _getKeyIv( pw );
const cipher = crypto.createCipheriv( 'aes192', stuff.key, stuff.iv );
encrypted = cipher.update( text, 'utf8', 'hex' );
encrypted += cipher.final( 'hex' );
return encrypted;
}
/**
* Decrypts encrypted text.
*
* @static
* @param { object } encrypted - The text to be decrypted
* @param { object } pw - The password to use for decryption
* @return { string } The decrypted result
*/
function insecureAes192Decrypt( encrypted, pw ) {
let decrypted;
const stuff = _getKeyIv( pw );
const decipher = crypto.createDecipheriv( 'aes192', stuff.key, stuff.iv );
decrypted = decipher.update( encrypted, 'hex', 'utf8' );
decrypted += decipher.final( 'utf8' );
return decrypted;
}
/**
* Returns random howMany-lengthed string from provided characters.
*
* @static
* @param { number } [howMany] - Desired length of string
* @param { string } [chars] - Characters to use
* @return { string } Random string
*/
function randomString( howMany = 8, chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' ) {
const rnd = crypto.randomBytes( howMany );
return new Array( howMany )
.fill() // create indices, so map can iterate
.map( ( val, i ) => chars[ rnd[ i ] % chars.length ] )
.join( '' );
}
/**
* Not secure, but used for backward compatibility with deprecated crypto.createCipher
* It's okay to use for this purpose.
*
* @param { string } pw - password
*/
function _getKeyIv( pw ) {
return EVP_BytesToKey( pw, null, 192, 16 );
}
/**
* Returns random item from array.
*
* @static
* @param {Array} array - Target array
* @return {*|null} Random array item
*/
function pickRandomItemFromArray( array ) {
if ( !Array.isArray( array ) || array.length === 0 ) {
return null;
}
const random = Math.floor( Math.random() * array.length );
if ( !array[ random ] ) {
return null;
}
return array[ random ];
}
/**
* Compares two objects by shallow properties.
*
* @static
* @param { object } a - First object to be compared
* @param { object } b - Second object to be compared
* @return {null|boolean} Whether objects are equal (`null` for invalid arguments)
*/
function areOwnPropertiesEqual( a, b ) {
let prop;
const results = [];
if ( typeof a !== 'object' || typeof b !== 'object' ) {
return null;
}
for ( prop in a ) {
if ( Object.prototype.hasOwnProperty.call( a, prop ) ) {
if ( a[ prop ] !== b[ prop ] ) {
return false;
}
results[ prop ] = true;
}
}
for ( prop in b ) {
if ( !results[ prop ] && Object.prototype.hasOwnProperty.call( b, prop ) ) {
if ( b[ prop ] !== a[ prop ] ) {
return false;
}
}
}
return true;
}
/**
* Converts a url to a local (proxied) url.
*
* @static
* @param { string } url - The url to convert
* @return { string } The converted url
*/
function toLocalMediaUrl( url ) {
const localUrl = `${config[ 'base path' ]}/media/get/${url.replace( /(https?):\/\//, '$1/' )}`;
return localUrl;
}
module.exports = {
getOpenRosaKey,
getXformsManifestHash,
cleanUrl,
isValidUrl,
md5,
randomString,
pickRandomItemFromArray,
areOwnPropertiesEqual,
insecureAes192Decrypt,
insecureAes192Encrypt,
toLocalMediaUrl
};